日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java 安全编程详解

發布時間:2024/3/7 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java 安全编程详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、加密與安全

在計算機系統中,什么是加密與安全呢?

我們舉個栗子:假設Bob要給Alice發一封郵件,在郵件傳送的過程中,黑客可能會竊取到郵件的內容,所以需要防竊聽。黑客還可能會篡改郵件的內容,Alice必須有能力識別出郵件有沒有被篡改。最后,黑客可能假冒Bob給Alice發郵件,Alice必須有能力識別出偽造的郵件。

所以,應對潛在的安全威脅,需要做到三防:

  • 防竊聽
  • 防篡改
  • 防偽造

計算機加密技術就是為了實現上述目標,而現代計算機密碼學理論是建立在嚴格的數學理論基礎上的,密碼學已經逐漸發展成一門科學。對于絕大多數開發者來說,設計一個安全的加密算法非常困難,驗證一個加密算法是否安全更加困難,當前被認為安全的加密算法僅僅是迄今為止尚未被攻破。因此,要編寫安全的計算機程序,我們要做到:

  • 不要自己設計山寨的加密算法;
  • 不要自己實現已有的加密算法;
  • 不要自己修改已有的加密算法。

我們將介紹最常用的加密算法,以及如何通過Java代碼實現。

二、編碼算法

1、編碼

要學習編碼算法,我們先來看一看什么是編碼。

ASCII碼就是一種編碼,字母A的編碼是十六進制的0x41,字母B是0x42,以此類推:

字母ASCII編碼
A0x41
B0x42
C0x43
D0x44

因為ASCII編碼最多只能有128個字符,要想對更多的文字進行編碼,就需要用Unicode。而中文的中使用Unicode編碼就是0x4e2d,使用UTF-8則需要3個字節編碼:

漢字Unicode編碼UTF-8編碼
0x4e2d0xe4b8ad
0x65870xe69687
0x7f160xe7bc96
0x78010xe7a081

因此,最簡單的編碼是直接給每個字符指定一個若干字節表示的整數,復雜一點的編碼就需要根據一個已有的編碼推算出來。

比如UTF-8編碼,它是一種不定長編碼,但可以從給定字符的Unicode編碼推算出來。

2、URL編碼

URL編碼是瀏覽器發送數據給服務器時使用的編碼,它通常附加在URL的參數部分,例如:

https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87

之所以需要URL編碼,是因為出于兼容性考慮,很多服務器只識別ASCII字符。但如果URL中包含中文、日文這些非ASCII字符怎么辦?不要緊,URL編碼有一套規則:

如果字符是A~Z,a~z,0~9以及-、_、.、*,則保持不變;
如果是其他字符,先轉換為UTF-8編碼,然后對每個字節以%XX表示。
例如:字符中的UTF-8編碼是0xe4b8ad,因此,它的URL編碼是%E4%B8%AD。URL編碼總是大寫。

Java標準庫提供了一個URLEncoder類來對任意字符串進行URL編碼:

import java.net.URLEncoder; import java.nio.charset.StandardCharsets;public class Main {public static void main(String[] args) {String encoded = URLEncoder.encode("中文!", StandardCharsets.UTF_8);System.out.println(encoded);} }

上述代碼的運行結果是%E4%B8%AD%E6%96%87%21,中的URL編碼是%E4%B8%AD,文的URL編碼是%E6%96%87,!雖然是ASCII字符,也要對其編碼為%21。

和標準的URL編碼稍有不同,URLEncoder把空格字符編碼成+,而現在的URL編碼標準要求空格被編碼為%20,不過,服務器都可以處理這兩種情況。

如果服務器收到URL編碼的字符串,就可以對其進行解碼,還原成原始字符串。Java標準庫的URLDecoder就可以解碼:

import java.net.URLDecoder; import java.nio.charset.StandardCharsets;public class Main {public static void main(String[] args) {String decoded = URLDecoder.decode("%E4%B8%AD%E6%96%87%21", StandardCharsets.UTF_8);System.out.println(decoded);} }

要特別注意:URL編碼是編碼算法,不是加密算法。URL編碼的目的是把任意文本數據編碼為%前綴表示的文本,編碼后的文本僅包含A~Z,a~z,0~9,-,_,.,*和%,便于瀏覽器和服務器處理。

3、Base64編碼

URL編碼是對字符進行編碼,表示成%xx的形式,而Base64編碼是對二進制數據進行編碼,表示成文本格式。

Base64編碼可以把任意長度的二進制數據變為純文本,且只包含A~Z、a~z、0~9、+、/、=這些字符。它的原理是把3字節的二進制數據按6bit一組,用4個int整數表示,然后查表,把int整數用索引對應到字符,得到編碼后的字符串。

舉個例子:3個byte數據分別是e4、b8、ad,按6bit分組得到39、0b、22和2d:

因為6位整數的范圍總是0~63,所以,能用64個字符表示:字符A~Z對應索引0~25,字符a~z對應索引26~51,字符0~9對應索引52~61,最后兩個索引62、63分別用字符+和/表示。

在Java中,二進制數據就是byte[]數組。Java標準庫提供了Base64來對byte[]數組進行編解碼:

import java.util.*;public class Main {public static void main(String[] args) {byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };String b64encoded = Base64.getEncoder().encodeToString(input);System.out.println(b64encoded);} }

編碼后得到5Lit4個字符。要對Base64解碼,仍然用Base64這個類:

import java.util.*;public class Main {public static void main(String[] args) {byte[] output = Base64.getDecoder().decode("5Lit");System.out.println(Arrays.toString(output)); // [-28, -72, -83]} }

有的童鞋會問:如果輸入的byte[]數組長度不是3的整數倍腫么辦?這種情況下,需要對輸入的末尾補一個或兩個0x00,編碼后,在結尾加一個=表示補充了1個0x00,加兩個=表示補充了2個0x00,解碼的時候,去掉末尾補充的一個或兩個0x00即可。

實際上,因為編碼后的長度加上=總是4的倍數,所以即使不加=也可以計算出原始輸入的byte[]。Base64編碼的時候可以用withoutPadding()去掉=,解碼出來的結果是一樣的:

import java.util.*;public class Main {public static void main(String[] args) {byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad, 0x21 };String b64encoded = Base64.getEncoder().encodeToString(input);String b64encoded2 = Base64.getEncoder().withoutPadding().encodeToString(input);System.out.println(b64encoded);System.out.println(b64encoded2);byte[] output = Base64.getDecoder().decode(b64encoded2);System.out.println(Arrays.toString(output));} }

因為標準的Base64編碼會出現+、/和=,所以不適合把Base64編碼后的字符串放到URL中。一種針對URL的Base64編碼可以在URL中使用的Base64編碼,它僅僅是把+變成-,/變成_:

import java.util.*;public class Main {public static void main(String[] args) {byte[] input = new byte[] { 0x01, 0x02, 0x7f, 0x00 };String b64encoded = Base64.getUrlEncoder().encodeToString(input);System.out.println(b64encoded);byte[] output = Base64.getUrlDecoder().decode(b64encoded);System.out.println(Arrays.toString(output));} }

Base64編碼的目的是把二進制數據變成文本格式,這樣在很多文本中就可以處理二進制數據。例如,電子郵件協議就是文本協議,如果要在電子郵件中添加一個二進制文件,就可以用Base64編碼,然后以文本的形式傳送。

Base64編碼的缺點是傳輸效率會降低,因為它把原始數據的長度增加了1/3。

和URL編碼一樣,Base64編碼是一種編碼算法,不是加密算法。

如果把Base64的64個字符編碼表換成32個、48個或者58個,就可以使用Base32編碼,Base48編碼和Base58編碼。字符越少,編碼的效率就會越低。

總結:

  • URL編碼和Base64編碼都是編碼算法,它們不是加密算法;
  • URL編碼的目的是把任意文本數據編碼為%前綴表示的文本,便于瀏覽器和服務器處理;
  • Base64編碼的目的是把任意二進制數據編碼為文本,但編碼后數據量會增加1/3。

三、哈希算法

1、哈希算法簡介

哈希算法(Hash)又稱摘要算法(Digest),它的作用是:對任意一組輸入數據進行計算,得到一個固定長度的輸出摘要。

哈希算法最重要的特點就是:

  • 相同的輸入一定得到相同的輸出;
  • 不同的輸入大概率得到不同的輸出。

哈希算法的目的就是為了驗證原始數據是否被篡改。

Java字符串的hashCode()就是一個哈希算法,它的輸入是任意字符串,輸出是固定的4字節int整數:

"hello".hashCode(); // 0x5e918d2 "hello, java".hashCode(); // 0x7a9d88e8 "hello, bob".hashCode(); // 0xa0dbae2f

兩個相同的字符串永遠會計算出相同的hashCode,否則基于hashCode定位的HashMap就無法正常工作。這也是為什么當我們自定義一個class時,覆寫equals()方法時我們必須正確覆寫hashCode()方法。

2、哈希碰撞

哈希碰撞是指,兩個不同的輸入得到了相同的輸出:

"AaAaAa".hashCode(); // 0x7460e8c0 "BBAaBB".hashCode(); // 0x7460e8c0

有童鞋會問:碰撞能不能避免?答案是不能。碰撞是一定會出現的,因為輸出的字節長度是固定的,String的hashCode()輸出是4字節整數,最多只有4294967296種輸出,但輸入的數據長度是不固定的,有無數種輸入。所以,哈希算法是把一個無限的輸入集合映射到一個有限的輸出集合,必然會產生碰撞。

碰撞不可怕,我們擔心的不是碰撞,而是碰撞的概率,因為碰撞概率的高低關系到哈希算法的安全性。一個安全的哈希算法必須滿足:

  • 碰撞概率低;
  • 不能猜測輸出。

不能猜測輸出是指,輸入的任意一個bit的變化會造成輸出完全不同,這樣就很難從輸出反推輸入(只能依靠暴力窮舉)。假設一種哈希算法有如下規律:

hashA("java001") = "123456" hashA("java002") = "123457" hashA("java003") = "123458"

那么很容易從輸出123459反推輸入,這種哈希算法就不安全。安全的哈希算法從輸出是看不出任何規律的:

hashB("java001") = "123456" hashB("java002") = "580271" hashB("java003") = ???

常用的哈希算法有:

算法輸出長度(位)輸出長度(字節)
MD5128 bits16 bytes
SHA-1160 bits20 bytes
RipeMD-160160 bits20 bytes
SHA-256256 bits32 bytes
SHA-512512 bits64 bytes

根據碰撞概率,哈希算法的輸出長度越長,就越難產生碰撞,也就越安全。

Java標準庫提供了常用的哈希算法,并且有一套統一的接口。我們以MD5算法為例,看看如何對輸入計算哈希:

import java.math.BigInteger; import java.security.MessageDigest;public class Main {public static void main(String[] args) throws Exception {// 創建一個MessageDigest實例:MessageDigest md = MessageDigest.getInstance("MD5");// 反復調用update輸入數據:md.update("Hello".getBytes("UTF-8"));md.update("World".getBytes("UTF-8"));byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6System.out.println(new BigInteger(1, result).toString(16));} }

使用MessageDigest時,我們首先根據哈希算法獲取一個MessageDigest實例,然后,反復調用update(byte[])輸入數據。當輸入結束后,調用digest()方法獲得byte[]數組表示的摘要,最后,把它轉換為十六進制的字符串。

運行上述代碼,可以得到輸入HelloWorld的MD5是68e109f0f40ca72a15e05cc22786f8e6。

3、哈希算法的用途

因為相同的輸入永遠會得到相同的輸出,因此,如果輸入被修改了,得到的輸出就會不同。

我們在網站上下載軟件的時候,經常看到下載頁顯示的哈希:

如何判斷下載到本地的軟件是原始的、未經篡改的文件?我們只需要自己計算一下本地文件的哈希值,再與官網公開的哈希值對比,如果相同,說明文件下載正確,否則,說明文件已被篡改。

哈希算法的另一個重要用途是存儲用戶口令。如果直接將用戶的原始口令存放到數據庫中,會產生極大的安全風險:

  • 數據庫管理員能夠看到用戶明文口令;
  • 數據庫數據一旦泄漏,黑客即可獲取用戶明文口令。

不存儲用戶的原始口令,那么如何對用戶進行認證?

方法是存儲用戶口令的哈希,例如,MD5。

在用戶輸入原始口令后,系統計算用戶輸入的原始口令的MD5并與數據庫存儲的MD5對比,如果一致,說明口令正確,否則,口令錯誤。

因此,數據庫存儲用戶名和口令的表內容應該像下面這樣:

sernamepassword
bobf30aa7a662c728b7407c54ae6bfd27d1
alice25d55ad283aa400af464c76d713c07ad
timbed128365216c019988915ed3add75fb

這樣一來,數據庫管理員看不到用戶的原始口令。即使數據庫泄漏,黑客也無法拿到用戶的原始口令。想要拿到用戶的原始口令,必須用暴力窮舉的方法,一個口令一個口令地試,直到某個口令計算的MD5恰好等于指定值。

使用哈希口令時,還要注意防止彩虹表攻擊。

什么是彩虹表呢?上面講到了,如果只拿到MD5,從MD5反推明文口令,只能使用暴力窮舉的方法。

然而黑客并不笨,暴力窮舉會消耗大量的算力和時間。但是,如果有一個預先計算好的常用口令和它們的MD5的對照表:

常用口令MD5
hello123f30aa7a662c728b7407c54ae6bfd27d1
1234567825d55ad283aa400af464c76d713c07ad
passw0rdbed128365216c019988915ed3add75fb
19700101570da6d5277a646f6552b8832012f5dc
202012316879c0ae9117b50074ce0a0d4c843060

這個表就是彩虹表。如果用戶使用了常用口令,黑客從MD5一下就能反查到原始口令:

bob的MD5:f30aa7a662c728b7407c54ae6bfd27d1,原始口令:hello123;

alice的MD5:25d55ad283aa400af464c76d713c07ad,原始口令:12345678;

tim的MD5:bed128365216c019988915ed3add75fb,原始口令:passw0rd。

這就是為什么不要使用常用密碼,以及不要使用生日作為密碼的原因。

即使用戶使用了常用口令,我們也可以采取措施來抵御彩虹表攻擊,方法是對每個口令額外添加隨機數,這個方法稱之為加鹽(salt):

digest = md5(salt+inputPassword)

經過加鹽處理的數據庫表,內容如下:

usernamesaltpassword
bobH1r0aa5022319ff4c56955e22a74abcc2c210
alice7$p2we5de688c99e961ed6e560b972dab8b6a
timz5Sk91eee304b92dc0d105904e7ab58fd2f64

加鹽的目的在于使黑客的彩虹表失效,即使用戶使用常用口令,也無法從MD5反推原始口令。

4、SHA-1

SHA-1也是一種哈希算法,它的輸出是160 bits,即20字節。SHA-1是由美國國家安全局開發的,SHA算法實際上是一個系列,包括SHA-0(已廢棄)、SHA-1、SHA-256、SHA-512等。

在Java中使用SHA-1,和MD5完全一樣,只需要把算法名稱改為"SHA-1":

import java.math.BigInteger; import java.security.MessageDigest;public class Main {public static void main(String[] args) throws Exception {// 創建一個MessageDigest實例:MessageDigest md = MessageDigest.getInstance("SHA-1");// 反復調用update輸入數據:md.update("Hello".getBytes("UTF-8"));md.update("World".getBytes("UTF-8"));byte[] result = md.digest(); // 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2System.out.println(new BigInteger(1, result).toString(16));} }

類似的,計算SHA-256,我們需要傳入名稱"SHA-256",計算SHA-512,我們需要傳入名稱"SHA-512"。Java標準庫支持的所有哈希算法可以在這里查到。

?注意:MD5因為輸出長度較短,短時間內破解是可能的,目前已經不推薦使用。

總結:

  • 哈希算法可用于驗證數據完整性,具有防篡改檢測的功能;
  • 常用的哈希算法有MD5、SHA-1等;
  • 用哈希存儲口令時要考慮彩虹表攻擊。

四、BouncyCastle

我們知道,Java標準庫提供了一系列常用的哈希算法。

但如果我們要用的某種算法,Java標準庫沒有提供怎么辦?

方法一:自己寫一個,難度很大;

方法二:找一個現成的第三方庫,直接使用。

BouncyCastle就是一個提供了很多哈希算法和加密算法的第三方庫。它提供了Java標準庫沒有的一些算法,例如,RipeMD160哈希算法。

我們來看一下如何使用BouncyCastle這個第三方提供的算法。

首先,我們必須把BouncyCastle提供的jar包放到classpath中。這個jar包就是bcprov-jdk18on-xxx.jar,可以從官方網站下載。

Java標準庫的java.security包提供了一種標準機制,允許第三方提供商無縫接入。我們要使用BouncyCastle提供的RipeMD160算法,需要先把BouncyCastle注冊一下:

public class Main {public static void main(String[] args) throws Exception {// 注冊BouncyCastle:Security.addProvider(new BouncyCastleProvider());// 按名稱正常調用:MessageDigest md = MessageDigest.getInstance("RipeMD160");md.update("HelloWorld".getBytes("UTF-8"));byte[] result = md.digest();System.out.println(new BigInteger(1, result).toString(16));} }

其中,注冊BouncyCastle是通過下面的語句實現的:

Security.addProvider(new BouncyCastleProvider());

注冊只需要在啟動時進行一次,后續就可以使用BouncyCastle提供的所有哈希算法和加密算法。

總結:

  • BouncyCastle是一個開源的第三方算法提供商;
  • BouncyCastle提供了很多Java標準庫沒有提供的哈希算法和加密算法;
  • 使用第三方算法前需要通過Security.addProvider()注冊。

五、Hmac算法

在前面講到哈希算法時,我們說,存儲用戶的哈希口令時,要加鹽存儲,目的就在于抵御彩虹表攻擊。

我們回顧一下哈希算法:

digest = hash(input)

正是因為相同的輸入會產生相同的輸出,我們加鹽的目的就在于,使得輸入有所變化:

digest = hash(salt + input)

這個salt可以看作是一個額外的“認證碼”,同樣的輸入,不同的認證碼,會產生不同的輸出。因此,要驗證輸出的哈希,必須同時提供“認證碼”。

Hmac算法就是一種基于密鑰的消息認證碼算法,它的全稱是Hash-based Message Authentication Code,是一種更安全的消息摘要算法。

Hmac算法總是和某種哈希算法配合起來用的。例如,我們使用MD5算法,對應的就是HmacMD5算法,它相當于“加鹽”的MD5:

HmacMD5 ≈ md5(secure_random_key, input)

因此,HmacMD5可以看作帶有一個安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好處:

  • HmacMD5使用的key長度是64字節,更安全;
  • Hmac是標準算法,同樣適用于SHA-1等其他哈希算法;
  • Hmac輸出和原有的哈希算法長度一致。

可見,Hmac本質上就是把key混入摘要的算法。驗證此哈希時,除了原始的輸入數據,還要提供key。

為了保證安全,我們不會自己指定key,而是通過Java標準庫的KeyGenerator生成一個安全的隨機的key。

下面是使用HmacMD5的代碼:

import java.math.BigInteger; import javax.crypto.*;public class Main {public static void main(String[] args) throws Exception {KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");SecretKey key = keyGen.generateKey();// 打印隨機生成的key:byte[] skey = key.getEncoded();System.out.println(new BigInteger(1, skey).toString(16));Mac mac = Mac.getInstance("HmacMD5");mac.init(key);mac.update("HelloWorld".getBytes("UTF-8"));byte[] result = mac.doFinal();System.out.println(new BigInteger(1, result).toString(16));} }

和MD5相比,使用HmacMD5的步驟是:

  • 通過名稱HmacMD5獲取KeyGenerator實例;
  • 通過KeyGenerator創建一個SecretKey實例;
  • 通過名稱HmacMD5獲取Mac實例;
  • 用SecretKey初始化Mac實例;
  • 對Mac實例反復調用update(byte[])輸入數據;
  • 調用Mac實例的doFinal()獲取最終的哈希值。
  • 我們可以用Hmac算法取代原有的自定義的加鹽算法,因此,存儲用戶名和口令的數據庫結構如下:

    usernamesecret_key (64 bytes)password
    boba8c06e05f92e...5e167e0387872a57c85ef6dddbaa12f376de
    alicee6a343693985...f4bec1f929ac2552642b302e739bc0cdbaac
    timf27a973dfdc0...6003af57651c3a8a73303515804d4af43790

    有了Hmac計算的哈希和SecretKey,我們想要驗證怎么辦?這時,SecretKey不能從KeyGenerator生成,而是從一個byte[]數組恢復:

    import java.util.Arrays; import javax.crypto.*; import javax.crypto.spec.*;public class Main {public static void main(String[] args) throws Exception {byte[] hkey = new byte[] { 106, 70, -110, 125, 39, -20, 52, 56, 85, 9, -19, -72, 52, -53, 52, -45, -6, 119, -63,30, 20, -83, -28, 77, 98, 109, -32, -76, 121, -106, 0, -74, -107, -114, -45, 104, -104, -8, 2, 121, 6,97, -18, -13, -63, -30, -125, -103, -80, -46, 113, -14, 68, 32, -46, 101, -116, -104, -81, -108, 122,89, -106, -109 };SecretKey key = new SecretKeySpec(hkey, "HmacMD5");Mac mac = Mac.getInstance("HmacMD5");mac.init(key);mac.update("HelloWorld".getBytes("UTF-8"));byte[] result = mac.doFinal();System.out.println(Arrays.toString(result));// [126, 59, 37, 63, 73, 90, 111, -96, -77, 15, 82, -74, 122, -55, -67, 54]} }

    恢復SecretKey的語句就是new SecretKeySpec(hkey, "HmacMD5")。

    ?總結:

    • Hmac算法是一種標準的基于密鑰的哈希算法,可以配合MD5、SHA-1等哈希算法,計算的摘要長度和原摘要算法長度相同。

    六、對稱加密算法

    1、對稱加密算法簡介

    對稱加密算法就是傳統的用一個密碼進行加密和解密。例如,我們常用的WinZIP和WinRAR對壓縮包的加密和解密,就是使用對稱加密算法:

    從程序的角度看,所謂加密,就是這樣一個函數,它接收密碼和明文,然后輸出密文:?

    secret = encrypt(key, message);

    而解密則相反,它接收密碼和密文,然后輸出明文:

    plain = decrypt(key, secret);

    在軟件開發中,常用的對稱加密算法有:

    算法密鑰長度工作模式填充模式
    DES56/64ECB/CBC/PCBC/CTR/...NoPadding/PKCS5Padding/...
    AES128/192/256ECB/CBC/PCBC/CTR/...NoPadding/PKCS5Padding/PKCS7Padding/...
    IDEA128ECBPKCS5Padding/PKCS7Padding/...

    密鑰長度直接決定加密強度,而工作模式和填充模式可以看成是對稱加密算法的參數和格式選擇。Java標準庫提供的算法實現并不包括所有的工作模式和所有填充模式,但是通常我們只需要挑選常用的使用就可以了。

    最后注意,DES算法由于密鑰過短,可以在短時間內被暴力破解,所以現在已經不安全了。

    2、使用AES加密

    AES算法是目前應用最廣泛的加密算法。我們先用ECB模式加密并解密:

    import java.security.*; import java.util.Base64;import javax.crypto.*; import javax.crypto.spec.*;public class Main {public static void main(String[] args) throws Exception {// 原文:String message = "Hello, world!";System.out.println("Message: " + message);// 128位密鑰 = 16 bytes Key:byte[] key = "1234567890abcdef".getBytes("UTF-8");// 加密:byte[] data = message.getBytes("UTF-8");byte[] encrypted = encrypt(key, data);System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));// 解密:byte[] decrypted = decrypt(key, encrypted);System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");SecretKey keySpec = new SecretKeySpec(key, "AES");cipher.init(Cipher.ENCRYPT_MODE, keySpec);return cipher.doFinal(input);}// 解密:public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");SecretKey keySpec = new SecretKeySpec(key, "AES");cipher.init(Cipher.DECRYPT_MODE, keySpec);return cipher.doFinal(input);} }

    Java標準庫提供的對稱加密接口非常簡單,使用時按以下步驟編寫代碼:

  • 根據算法名稱/工作模式/填充模式獲取Cipher實例;
  • 根據算法名稱初始化一個SecretKey實例,密鑰必須是指定長度;
  • 使用SerectKey初始化Cipher實例,并設置加密或解密模式;
  • 傳入明文或密文,獲得密文或明文。
  • ECB模式是最簡單的AES加密模式,它只需要一個固定長度的密鑰,固定的明文會生成固定的密文,這種一對一的加密方式會導致安全性降低,更好的方式是通過CBC模式,它需要一個隨機數作為IV參數,這樣對于同一份明文,每次生成的密文都不同:

    import java.security.*; import java.util.Base64; import javax.crypto.*; import javax.crypto.spec.*;public class Main {public static void main(String[] args) throws Exception {// 原文:String message = "Hello, world!";System.out.println("Message: " + message);// 256位密鑰 = 32 bytes Key:byte[] key = "1234567890abcdef1234567890abcdef".getBytes("UTF-8");// 加密:byte[] data = message.getBytes("UTF-8");byte[] encrypted = encrypt(key, data);System.out.println("Encrypted: " + Base64.getEncoder().encodeToString(encrypted));// 解密:byte[] decrypted = decrypt(key, encrypted);System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");SecretKeySpec keySpec = new SecretKeySpec(key, "AES");// CBC模式需要生成一個16 bytes的initialization vector:SecureRandom sr = SecureRandom.getInstanceStrong();byte[] iv = sr.generateSeed(16);IvParameterSpec ivps = new IvParameterSpec(iv);cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);byte[] data = cipher.doFinal(input);// IV不需要保密,把IV和密文一起返回:return join(iv, data);}// 解密:public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 把input分割成IV和密文:byte[] iv = new byte[16];byte[] data = new byte[input.length - 16];System.arraycopy(input, 0, iv, 0, 16);System.arraycopy(input, 16, data, 0, data.length);// 解密:Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");SecretKeySpec keySpec = new SecretKeySpec(key, "AES");IvParameterSpec ivps = new IvParameterSpec(iv);cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);return cipher.doFinal(data);}public static byte[] join(byte[] bs1, byte[] bs2) {byte[] r = new byte[bs1.length + bs2.length];System.arraycopy(bs1, 0, r, 0, bs1.length);System.arraycopy(bs2, 0, r, bs1.length, bs2.length);return r;} }

    在CBC模式下,需要一個隨機生成的16字節IV參數,必須使用SecureRandom生成。因為多了一個IvParameterSpec實例,因此,初始化方法需要調用Cipher的一個重載方法并傳入IvParameterSpec。

    觀察輸出,可以發現每次生成的IV不同,密文也不同。

    總結:

    • 對稱加密算法使用同一個密鑰進行加密和解密,常用算法有DES、AES和IDEA等;
    • 密鑰長度由算法設計決定,AES的密鑰長度是128/192/256位;
    • 使用對稱加密算法需要指定算法名稱、工作模式和填充模式。

    七、口令加密算法

    我們已經講的AES加密,細心的童鞋可能會發現,密鑰長度是固定的128/192/256位,而不是我們用WinZip/WinRAR那樣,隨便輸入幾位都可以。

    這是因為對稱加密算法決定了口令必須是固定長度,然后對明文進行分塊加密。又因為安全需求,口令長度往往都是128位以上,即至少16個字符。

    但是我們平時使用的加密軟件,輸入6位、8位都可以,難道加密方式不一樣?

    實際上用戶輸入的口令并不能直接作為AES的密鑰進行加密(除非長度恰好是128/192/256位),并且用戶輸入的口令一般都有規律,安全性遠遠不如安全隨機數產生的隨機口令。因此,用戶輸入的口令,通常還需要使用PBE算法,采用隨機數雜湊計算出真正的密鑰,再進行加密。

    PBE就是Password Based Encryption的縮寫,它的作用如下:

    key = generate(userPassword, secureRandomPassword);

    PBE的作用就是把用戶輸入的口令和一個安全隨機的口令采用雜湊后計算出真正的密鑰。以AES密鑰為例,我們讓用戶輸入一個口令,然后生成一個隨機數,通過PBE算法計算出真正的AES口令,再進行加密,代碼如下:

    public class Main {public static void main(String[] args) throws Exception {// 把BouncyCastle作為Provider添加到java.security:Security.addProvider(new BouncyCastleProvider());// 原文:String message = "Hello, world!";// 加密口令:String password = "hello12345";// 16 bytes隨機Salt:byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);System.out.printf("salt: %032x\n", new BigInteger(1, salt));// 加密:byte[] data = message.getBytes("UTF-8");byte[] encrypted = encrypt(password, salt, data);System.out.println("encrypted: " + Base64.getEncoder().encodeToString(encrypted));// 解密:byte[] decrypted = decrypt(password, salt, encrypted);System.out.println("decrypted: " + new String(decrypted, "UTF-8"));}// 加密:public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");SecretKey skey = skeyFactory.generateSecret(keySpec);PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);return cipher.doFinal(input);}// 解密:public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");SecretKey skey = skeyFactory.generateSecret(keySpec);PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);return cipher.doFinal(input);} }

    使用PBE時,我們還需要引入BouncyCastle,并指定算法是PBEwithSHA1and128bitAES-CBC-BC。觀察代碼,實際上真正的AES密鑰是調用Cipher的init()方法時同時傳入SecretKey和PBEParameterSpec實現的。在創建PBEParameterSpec的時候,我們還指定了循環次數1000,循環次數越多,暴力破解需要的計算量就越大。

    如果我們把salt和循環次數固定,就得到了一個通用的“口令”加密軟件。如果我們把隨機生成的salt存儲在U盤,就得到了一個“口令”加USB Key的加密軟件,它的好處在于,即使用戶使用了一個非常弱的口令,沒有USB Key仍然無法解密,因為USB Key存儲的隨機數密鑰安全性非常高。

    總結:

    • PBE算法通過用戶口令和安全的隨機salt計算出Key,然后再進行加密;
    • Key通過口令和安全的隨機salt計算得出,大大提高了安全性;
    • PBE算法內部使用的仍然是標準對稱加密算法(例如AES);

    八、密鑰交換算法

    對稱加密算法解決了數據加密的問題。我們以AES加密為例,在現實世界中,小明要向路人甲發送一個加密文件,他可以先生成一個AES密鑰,對文件進行加密,然后把加密文件發送給對方。因為對方要解密,就必須需要小明生成的密鑰。

    現在問題來了:如何傳遞密鑰?

    在不安全的信道上傳遞加密文件是沒有問題的,因為黑客拿到加密文件沒有用。但是,如何如何在不安全的信道上安全地傳輸密鑰?

    要解決這個問題,密鑰交換算法即DH算法:Diffie-Hellman算法應運而生。

    DH算法解決了密鑰在雙方不直接傳遞密鑰的情況下完成密鑰交換,這個神奇的交換原理完全由數學理論支持。

    我們來看DH算法交換密鑰的步驟。假設甲乙雙方需要傳遞密鑰,他們之間可以這么做:

  • 甲首選選擇一個素數p,例如509,底數g,任選,例如5,隨機數a,例如123,然后計算A=g^a mod p,結果是215,然后,甲發送p=509,g=5,A=215給乙;
  • 乙方收到后,也選擇一個隨機數b,例如,456,然后計算B=g^b mod p,結果是181,乙再同時計算s=A^b mod p,結果是121;
  • 乙把計算的B=181發給甲,甲計算s=B^a mod p的余數,計算結果與乙算出的結果一樣,都是121。
  • 所以最終雙方協商出的密鑰s是121。注意到這個密鑰s并沒有在網絡上傳輸。而通過網絡傳輸的p,g,A和B是無法推算出s的,因為實際算法選擇的素數是非常大的。

    所以,更確切地說,DH算法是一個密鑰協商算法,雙方最終協商出一個共同的密鑰,而這個密鑰不會通過網絡傳輸。

    如果我們把a看成甲的私鑰,A看成甲的公鑰,b看成乙的私鑰,B看成乙的公鑰,DH算法的本質就是雙方各自生成自己的私鑰和公鑰,私鑰僅對自己可見,然后交換公鑰,并根據自己的私鑰和對方的公鑰,生成最終的密鑰secretKey,DH算法通過數學定律保證了雙方各自計算出的secretKey是相同的。

    使用Java實現DH算法的代碼如下:

    import java.math.BigInteger; import java.security.*; import java.security.spec.*;import javax.crypto.KeyAgreement;public class Main {public static void main(String[] args) {// Bob和Alice:Person bob = new Person("Bob");Person alice = new Person("Alice");// 各自生成KeyPair:bob.generateKeyPair();alice.generateKeyPair();// 雙方交換各自的PublicKey:// Bob根據Alice的PublicKey生成自己的本地密鑰:bob.generateSecretKey(alice.publicKey.getEncoded());// Alice根據Bob的PublicKey生成自己的本地密鑰:alice.generateSecretKey(bob.publicKey.getEncoded());// 檢查雙方的本地密鑰是否相同:bob.printKeys();alice.printKeys();// 雙方的SecretKey相同,后續通信將使用SecretKey作為密鑰進行AES加解密...} }class Person {public final String name;public PublicKey publicKey;private PrivateKey privateKey;private byte[] secretKey;public Person(String name) {this.name = name;}// 生成本地KeyPair:public void generateKeyPair() {try {KeyPairGenerator kpGen = KeyPairGenerator.getInstance("DH");kpGen.initialize(512);KeyPair kp = kpGen.generateKeyPair();this.privateKey = kp.getPrivate();this.publicKey = kp.getPublic();} catch (GeneralSecurityException e) {throw new RuntimeException(e);}}public void generateSecretKey(byte[] receivedPubKeyBytes) {try {// 從byte[]恢復PublicKey:X509EncodedKeySpec keySpec = new X509EncodedKeySpec(receivedPubKeyBytes);KeyFactory kf = KeyFactory.getInstance("DH");PublicKey receivedPublicKey = kf.generatePublic(keySpec);// 生成本地密鑰:KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");keyAgreement.init(this.privateKey); // 自己的PrivateKeykeyAgreement.doPhase(receivedPublicKey, true); // 對方的PublicKey// 生成SecretKey密鑰:this.secretKey = keyAgreement.generateSecret();} catch (GeneralSecurityException e) {throw new RuntimeException(e);}}public void printKeys() {System.out.printf("Name: %s\n", this.name);System.out.printf("Private key: %x\n", new BigInteger(1, this.privateKey.getEncoded()));System.out.printf("Public key: %x\n", new BigInteger(1, this.publicKey.getEncoded()));System.out.printf("Secret key: %x\n", new BigInteger(1, this.secretKey));} }

    但是DH算法并未解決中間人攻擊,即甲乙雙方并不能確保與自己通信的是否真的是對方。消除中間人攻擊需要其他方法。

    總結:

    • DH算法是一種密鑰交換協議,通信雙方通過不安全的信道協商密鑰,然后進行對稱加密傳輸。
    • DH算法沒有解決中間人攻擊。

    九、非對稱加密算法

    從DH算法我們可以看到,公鑰-私鑰組成的密鑰對是非常有用的加密方式,因為公鑰是可以公開的,而私鑰是完全保密的,由此奠定了非對稱加密的基礎。

    非對稱加密就是加密和解密使用的不是相同的密鑰:只有同一個公鑰-私鑰對才能正常加解密。

    因此,如果小明要加密一個文件發送給小紅,他應該首先向小紅索取她的公鑰,然后,他用小紅的公鑰加密,把加密文件發送給小紅,此文件只能由小紅的私鑰解開,因為小紅的私鑰在她自己手里,所以,除了小紅,沒有任何人能解開此文件。

    非對稱加密的典型算法就是RSA算法,它是由Ron Rivest,Adi Shamir,Leonard Adleman這三個哥們一起發明的,所以用他們仨的姓的首字母縮寫表示。

    非對稱加密相比對稱加密的顯著優點在于,對稱加密需要協商密鑰,而非對稱加密可以安全地公開各自的公鑰,在N個人之間通信的時候:使用非對稱加密只需要N個密鑰對,每個人只管理自己的密鑰對。而使用對稱加密需要則需要N*(N-1)/2個密鑰,因此每個人需要管理N-1個密鑰,密鑰管理難度大,而且非常容易泄漏。

    既然非對稱加密這么好,那我們拋棄對稱加密,完全使用非對稱加密行不行?也不行。因為非對稱加密的缺點就是運算速度非常慢,比對稱加密要慢很多。

    所以,在實際應用的時候,非對稱加密總是和對稱加密一起使用。假設小明需要給小紅需要傳輸加密文件,他倆首先交換了各自的公鑰,然后:

  • 小明生成一個隨機的AES口令,然后用小紅的公鑰通過RSA加密這個口令,并發給小紅;
  • 小紅用自己的RSA私鑰解密得到AES口令;
  • 雙方使用這個共享的AES口令用AES加密通信。
  • 可見非對稱加密實際上應用在第一步,即加密“AES口令”。這也是我們在瀏覽器中常用的HTTPS協議的做法,即瀏覽器和服務器先通過RSA交換AES口令,接下來雙方通信實際上采用的是速度較快的AES對稱加密,而不是緩慢的RSA非對稱加密。

    Java標準庫提供了RSA算法的實現,示例代碼如下:

    import java.math.BigInteger; import java.security.*; import javax.crypto.Cipher;public class Main {public static void main(String[] args) throws Exception {// 明文:byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8");// 創建公鑰/私鑰對:Person alice = new Person("Alice");// 用Alice的公鑰加密:byte[] pk = alice.getPublicKey();System.out.println(String.format("public key: %x", new BigInteger(1, pk)));byte[] encrypted = alice.encrypt(plain);System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));// 用Alice的私鑰解密:byte[] sk = alice.getPrivateKey();System.out.println(String.format("private key: %x", new BigInteger(1, sk)));byte[] decrypted = alice.decrypt(encrypted);System.out.println(new String(decrypted, "UTF-8"));} }class Person {String name;// 私鑰:PrivateKey sk;// 公鑰:PublicKey pk;public Person(String name) throws GeneralSecurityException {this.name = name;// 生成公鑰/私鑰對:KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");kpGen.initialize(1024);KeyPair kp = kpGen.generateKeyPair();this.sk = kp.getPrivate();this.pk = kp.getPublic();}// 把私鑰導出為字節public byte[] getPrivateKey() {return this.sk.getEncoded();}// 把公鑰導出為字節public byte[] getPublicKey() {return this.pk.getEncoded();}// 用公鑰加密:public byte[] encrypt(byte[] message) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.ENCRYPT_MODE, this.pk);return cipher.doFinal(message);}// 用私鑰解密:public byte[] decrypt(byte[] input) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance("RSA");cipher.init(Cipher.DECRYPT_MODE, this.sk);return cipher.doFinal(input);} }

    RSA的公鑰和私鑰都可以通過getEncoded()方法獲得以byte[]表示的二進制數據,并根據需要保存到文件中。

    要從byte[]數組恢復公鑰或私鑰,可以這么寫:

    byte[] pkData = ... byte[] skData = ... KeyFactory kf = KeyFactory.getInstance("RSA"); // 恢復公鑰: X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pkData); PublicKey pk = kf.generatePublic(pkSpec); // 恢復私鑰: PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(skData); PrivateKey sk = kf.generatePrivate(skSpec);

    以RSA算法為例,它的密鑰有256/512/1024/2048/4096等不同的長度。長度越長,密碼強度越大,當然計算速度也越慢。

    如果修改待加密的byte[]數據的大小,可以發現,使用512bit的RSA加密時,明文長度不能超過53字節,使用1024bit的RSA加密時,明文長度不能超過117字節,這也是為什么使用RSA的時候,總是配合AES一起使用,即用AES加密任意長度的明文,用RSA加密AES口令。

    此外,只使用非對稱加密算法不能防止中間人攻擊。

    總結:

    • 非對稱加密就是加密和解密使用的不是相同的密鑰,只有同一個公鑰-私鑰對才能正常加解密;
    • 只使用非對稱加密算法不能防止中間人攻擊;

    十、簽名算法

    1、簽名簡介

    我們使用非對稱加密算法的時候,對于一個公鑰-私鑰對,通常是用公鑰加密,私鑰解密。

    如果使用私鑰加密,公鑰解密是否可行呢?實際上是完全可行的。

    不過我們再仔細想一想,私鑰是保密的,而公鑰是公開的,用私鑰加密,那相當于所有人都可以用公鑰解密。這個加密有什么意義?

    這個加密的意義在于,如果小明用自己的私鑰加密了一條消息,比如小明喜歡小紅,然后他公開了加密消息,由于任何人都可以用小明的公鑰解密,從而使得任何人都可以確認小明喜歡小紅這條消息肯定是小明發出的,其他人不能偽造這個消息,小明也不能抵賴這條消息不是自己寫的。

    因此,私鑰加密得到的密文實際上就是數字簽名,要驗證這個簽名是否正確,只能用私鑰持有者的公鑰進行解密驗證。使用數字簽名的目的是為了確認某個信息確實是由某個發送方發送的,任何人都不可能偽造消息,并且,發送方也不能抵賴。

    在實際應用的時候,簽名實際上并不是針對原始消息,而是針對原始消息的哈希進行簽名,即:

    signature = encrypt(privateKey, sha256(message))

    對簽名進行驗證實際上就是用公鑰解密:

    hash = decrypt(publicKey, signature)

    然后把解密后的哈希與原始消息的哈希進行對比。

    因為用戶總是使用自己的私鑰進行簽名,所以,私鑰就相當于用戶身份。而公鑰用來給外部驗證用戶身份。

    常用數字簽名算法有:

    • MD5withRSA
    • SHA1withRSA
    • SHA256withRSA

    它們實際上就是指定某種哈希算法進行RSA簽名的方式。

    import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*;public class Main {public static void main(String[] args) throws GeneralSecurityException {// 生成RSA公鑰/私鑰:KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");kpGen.initialize(1024);KeyPair kp = kpGen.generateKeyPair();PrivateKey sk = kp.getPrivate();PublicKey pk = kp.getPublic();// 待簽名的消息:byte[] message = "Hello, I am Bob!".getBytes(StandardCharsets.UTF_8);// 用私鑰簽名:Signature s = Signature.getInstance("SHA1withRSA");s.initSign(sk);s.update(message);byte[] signed = s.sign();System.out.println(String.format("signature: %x", new BigInteger(1, signed)));// 用公鑰驗證:Signature v = Signature.getInstance("SHA1withRSA");v.initVerify(pk);v.update(message);boolean valid = v.verify(signed);System.out.println("valid? " + valid);} }

    使用其他公鑰,或者驗證簽名的時候修改原始信息,都無法驗證成功。

    2、DSA簽名

    除了RSA可以簽名外,還可以使用DSA算法進行簽名。DSA是Digital Signature Algorithm的縮寫,它使用ElGamal數字簽名算法。

    DSA只能配合SHA使用,常用的算法有:

    • SHA1withDSA
    • SHA256withDSA
    • SHA512withDSA

    和RSA數字簽名相比,DSA的優點是更快。

    3、ECDSA簽名

    橢圓曲線簽名算法ECDSA:Elliptic Curve Digital Signature Algorithm也是一種常用的簽名算法,它的特點是可以從私鑰推出公鑰。比特幣的簽名算法就采用了ECDSA算法,使用標準橢圓曲線secp256k1。BouncyCastle提供了ECDSA的完整實現。

    數字簽名就是用發送方的私鑰對原始數據進行簽名,只有用發送方公鑰才能通過簽名驗證。

    數字簽名用于:

    • 防止偽造;
    • 防止抵賴;
    • 檢測篡改。

    常用的數字簽名算法包括:MD5withRSA/SHA1withRSA/SHA256withRSA/SHA1withDSA/SHA256withDSA/SHA512withDSA/ECDSA等。

    十一、數字證書

    我們知道,摘要算法用來確保數據沒有被篡改,非對稱加密算法可以對數據進行加解密,簽名算法可以確保數據完整性和抗否認性,把這些算法集合到一起,并搞一套完善的標準,這就是數字證書。

    因此,數字證書就是集合了多種密碼學算法,用于實現數據加解密、身份認證、簽名等多種功能的一種安全標準。

    數字證書可以防止中間人攻擊,因為它采用鏈式簽名認證,即通過根證書(Root CA)去簽名下一級證書,這樣層層簽名,直到最終的用戶證書。而Root CA證書內置于操作系統中,所以,任何經過CA認證的數字證書都可以對其本身進行校驗,確保證書本身不是偽造的。

    我們在上網時常用的HTTPS協議就是數字證書的應用,瀏覽器會自動驗證證書的有效性。

    要使用數字證書,首先需要創建證書。正常情況下,一個合法的數字證書需要經過CA簽名,這需要認證域名并支付一定的費用。開發的時候,我們可以使用自簽名的證書,這種證書可以正常開發調試,但不能對外作為服務使用,因為其他客戶端并不認可未經CA簽名的證書。

    注:騰訊云可申請有效期1年的免費SSL證書,Let's Encrypt可申請有效期90天的免費SSL證書。

    在Java程序中,數字證書存儲在一種Java專用的key store文件中,JDK提供了一系列命令來創建和管理key store。我們用下面的命令創建一個key store,并設定口令123456:

    keytool -storepass 123456 -genkeypair -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 3650 -alias mycert -keystore my.keystore -dname "CN=www.sample.com, OU=sample, O=sample, L=BJ, ST=BJ, C=CN"

    幾個主要的參數是:

    • keyalg:指定RSA加密算法;
    • sigalg:指定SHA1withRSA簽名算法;
    • validity:指定證書有效期3650天;
    • alias:指定證書在程序中引用的名稱;
    • dname:最重要的CN=www.sample.com指定了Common Name,如果證書用在HTTPS中,這個名稱必須與域名完全一致。

    執行上述命令,JDK會在當前目錄創建一個my.keystore文件,并存儲創建成功的一個私鑰和一個證書,它的別名是mycert。

    有了key store存儲的證書,我們就可以通過數字證書進行加解密和簽名:

    import java.io.InputStream; import java.math.BigInteger; import java.security.*; import java.security.cert.*; import javax.crypto.Cipher;public class Main {public static void main(String[] args) throws Exception {byte[] message = "Hello, use X.509 cert!".getBytes("UTF-8");// 讀取KeyStore:KeyStore ks = loadKeyStore("/my.keystore", "123456");// 讀取私鑰:PrivateKey privateKey = (PrivateKey) ks.getKey("mycert", "123456".toCharArray());// 讀取證書:X509Certificate certificate = (X509Certificate) ks.getCertificate("mycert");// 加密:byte[] encrypted = encrypt(certificate, message);System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));// 解密:byte[] decrypted = decrypt(privateKey, encrypted);System.out.println("decrypted: " + new String(decrypted, "UTF-8"));// 簽名:byte[] sign = sign(privateKey, certificate, message);System.out.println(String.format("signature: %x", new BigInteger(1, sign)));// 驗證簽名:boolean verified = verify(certificate, message, sign);System.out.println("verify: " + verified);}static KeyStore loadKeyStore(String keyStoreFile, String password) {try (InputStream input = Main.class.getResourceAsStream(keyStoreFile)) {if (input == null) {throw new RuntimeException("file not found in classpath: " + keyStoreFile);}KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());ks.load(input, password.toCharArray());return ks;} catch (Exception e) {throw new RuntimeException(e);}}static byte[] encrypt(X509Certificate certificate, byte[] message) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance(certificate.getPublicKey().getAlgorithm());cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());return cipher.doFinal(message);}static byte[] decrypt(PrivateKey privateKey, byte[] data) throws GeneralSecurityException {Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());cipher.init(Cipher.DECRYPT_MODE, privateKey);return cipher.doFinal(data);}static byte[] sign(PrivateKey privateKey, X509Certificate certificate, byte[] message)throws GeneralSecurityException {Signature signature = Signature.getInstance(certificate.getSigAlgName());signature.initSign(privateKey);signature.update(message);return signature.sign();}static boolean verify(X509Certificate certificate, byte[] message, byte[] sig) throws GeneralSecurityException {Signature signature = Signature.getInstance(certificate.getSigAlgName());signature.initVerify(certificate);signature.update(message);return signature.verify(sig);} }

    在上述代碼中,我們從key store直接讀取了私鑰-公鑰對,私鑰以PrivateKey實例表示,公鑰以X509Certificate表示,實際上數字證書只包含公鑰,因此,讀取證書并不需要口令,只有讀取私鑰才需要。如果部署到Web服務器上,例如Nginx,需要把私鑰導出為Private Key格式,把證書導出為X509Certificate格式。

    以HTTPS協議為例,瀏覽器和服務器建立安全連接的步驟如下:

  • 瀏覽器向服務器發起請求,服務器向瀏覽器發送自己的數字證書;
  • 瀏覽器用操作系統內置的Root CA來驗證服務器的證書是否有效,如果有效,就使用該證書加密一個隨機的AES口令并發送給服務器;
  • 服務器用自己的私鑰解密獲得AES口令,并在后續通訊中使用AES加密。
  • 上述流程只是一種最常見的單向驗證。如果服務器還要驗證客戶端,那么客戶端也需要把自己的證書發送給服務器驗證,這種場景常見于網銀等。

    注意:數字證書存儲的是公鑰,以及相關的證書鏈和算法信息。私鑰必須嚴格保密,如果數字證書對應的私鑰泄漏,就會造成嚴重的安全威脅。如果CA證書的私鑰泄漏,那么該CA證書簽發的所有證書將不可信。數字證書服務商DigiNotar就發生過私鑰泄漏導致公司破產的事故。

    總結:

    • 數字證書就是集合了多種密碼學算法,用于實現數據加解密、身份認證、簽名等多種功能的一種安全標準。
    • 數字證書采用鏈式簽名管理,頂級的Root CA證書已內置在操作系統中。
    • 數字證書存儲的是公鑰,可以安全公開,而私鑰必須嚴格保密。

    總結

    以上是生活随笔為你收集整理的Java 安全编程详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 亚洲精品一区中文字幕乱码 | 国产高清毛片 | 一区二区三区视频免费视 | 18岁免费观看电视连续剧 | 精品久久久久久久久久久久久 | 成人免费在线 | 97综合视频 | 亚洲成熟少妇视频在线观看 | 第一页在线 | 欧美性教育视频 | 中文在线字幕免费观看电 | 亚洲国产成人精品女人久久 | 在线观看成人免费 | 日韩欧美毛片 | 人人舔 | 91桃色在线 | 美女网站视频在线观看 | 日韩福利在线视频 | 新超碰97 | 在线观看jizz | 毛片基地站 | 欧美日本色 | 亚洲乱码中文字幕久久孕妇黑人 | 69视频免费观看 | www.色婷婷| 性生活三级视频 | 亚洲系列在线 | 中国三级黄色 | 撒尿free性hd | 又紧又大又爽精品一区二区 | 中国黄色1级片 | 国产精品免费av一区二区三区 | 亚洲乱码国产乱码精品精 | 国产影视av | 露出调教羞耻91九色 | 2022精品国偷自产免费观看 | 国产伦人伦偷精品视频 | 18做爰免费视频网站 | 13日本xxxxxⅹxxx20 | 日日网站 | 涩涩视频在线看 | 极品五月天 | 亚洲综合免费观看高清完整版 | 国产传媒一级片 | 日本高清视频网站 | 91精品91 | 亚洲v欧美v另类v综合v日韩v | 大香依人 | 日韩网红少妇无码视频香港 | 黄色精品一区 | 午夜黄视频| 九九热精品免费视频 | 久久精品女人 | 国产亚洲欧美在线 | av大片免费观看 | 夜av| 永久免费毛片 | 丰满白嫩尤物一区二区 | av999| 亚洲激情图 | 欧美性白人极品1819hd | av中文字幕亚洲 | 亚洲精品高清视频 | 免费看黄色一级片 | 国产精品视频专区 | 日本九九视频 | 成人午夜av | 最近中文在线观看 | 日韩sese| 人妻内射一区二区在线视频 | 欧美一级一片 | 97精品超碰一区二区三区 | 一区高清| av福利社| 色鬼久久 | 精品在线视频观看 | 午夜一级在线 | 亚洲最大中文字幕 | 亚洲看片网 | 欧美另类人妖 | 亚洲av综合色区无码一区 | 色牛影院 | 亚洲自拍第二页 | 北条麻妃一区二区三区免费 | 在线看黄色的网站 | 黄色录像一级大片 | 日本女人毛茸茸 | 欧美www.| 成年人在线免费观看 | 欧美日韩中文字幕一区二区 | 亚洲男人天堂电影 | 一级片在线免费观看视频 | 久久久久久网站 | 亚洲精品一区二区三区区别 | 日日摸日日碰夜夜爽av | wwwxxxx国产 | 撸撸在线视频 | 亚洲精品一区二区在线 | 神马伦理影视 |