Java安全——金鑰那些事

尊淵發表於2017-03-14

標籤(空格分隔): Java 安全

概念

金鑰是加密演算法不可缺少的部分。金鑰在安全體系中至關重要,正如其名,私密的鑰匙,開啟安全的大門。金鑰分兩種:對稱金鑰和非對稱金鑰。非對稱金鑰裡又包含公開金鑰和私有金鑰。

與金鑰相關的還有一個概念是證照。證照主要用於鑑別金鑰,通常將公開金鑰放到證照裡傳輸。

Java的安全體系裡,金鑰是通過JCE演算法包實現的。操作金鑰的引擎包含兩部分:金鑰生成器和金鑰工廠。金鑰生成器可以建立金鑰,而金鑰工廠將其進行包裝展示到外部。所以對於編寫程式來說,建立金鑰包括兩個步驟:1,用金鑰生成器產生金鑰;2,用金鑰工廠將其輸出為一個金鑰規範或者一組位元組碼。

Java實現

Java裡將金鑰封裝了一個介面——Key。非對稱金鑰有PublicKey和PrivateKey,均實現了該介面。從之前的“安全提供者框架”中的輸出結果可以看到,不同的安全提供者提供了很多金鑰生成演算法,比較典型的是Sun的DSA和RSA以及JCE的Diffie-Hellman演算法。

生成和表示key

金鑰的生成,Java提供了兩個生成器類——KeyPairGenerator和KeyGenerator,前者用於生成非對稱金鑰,後者用於生成對稱金鑰。對應金鑰的表示,KeyFactory類表示非對稱金鑰,SecretKeyFactory表示對稱金鑰。

我們來看一個DSA的例子:

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;

public class KeyTest {

    public static void main(String[] args) {
        try {
            generateKeyPair();
            generateKey();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    public static void generateKeyPair() throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(512);
        KeyPair kp = kpg.generateKeyPair();
        System.out.println(kpg.getProvider());
        System.out.println(kpg.getAlgorithm());
        KeyFactory kf = KeyFactory.getInstance("DSA");
        DSAPrivateKeySpec dsaPKS = kf.getKeySpec(kp.getPrivate(), DSAPrivateKeySpec.class);
        System.out.println("	DSA param G:" + dsaPKS.getG());
        System.out.println("	DSA param P:" + dsaPKS.getP());
        System.out.println("	DSA param Q:" + dsaPKS.getQ());
        System.out.println("	DSA param X:" + dsaPKS.getX());
    }

    public static void generateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyGenerator kg = KeyGenerator.getInstance("DES");
        SecretKey key = kg.generateKey();
        System.out.println(kg.getProvider());
        System.out.println(kg.getAlgorithm());
        SecretKeyFactory skf = SecretKeyFactory.getInstance("DES");
        DESKeySpec desKS = (DESKeySpec) skf.getKeySpec(key, DESKeySpec.class);
        System.out.println("	DES key bytes size:" + desKS.getKey().length);
    }

}

Key生成的程式碼架構設計類圖如下:
%5b%26lt%3b%26lt%3bKey%26gt%3b%26gt%3b%5

KeyGenerator與KPG類似,只是KPG生成KeyPair,KG
生成SecretKey。

金鑰管理

關於證照

證照這東西,真不知道放哪裡合適,就不單獨拿出來講了。考慮到證照可以驗證金鑰的合法性,就放這裡說一下吧。

因為非對稱金鑰的場景,需要將公鑰傳輸給對應的需求者。那麼如何傳輸能確保這個公鑰是我給你的而不是別人替換的呢?那就需要對這次傳輸加密簽名,於是又進入了這樣的迴圈。於是就引出了證照——證照可以保證內容和發源地是一致的,也就是說證照可以保證發給需求者的內容確實是屬於內容擁有者的。

證照不是誰都能來發的,證照是通過一個公正實體(CA,證照授權機構)來頒發並驗證合法性。證照包含三方面內容:
1,實體名,即證照持有者。
2,與主體相關的公開金鑰。
3,驗證證照資訊的數字簽名。證照由證照發行人簽名。
Java中有對應的Certificate類來做證照相關的事情。但是證照不是我們這裡要討論的重點,而且Java本身對證照的支援就不完備。因此證照的內容就在這裡插播一下。我們還是回到金鑰的傳輸問題上來。

KeyStore

Java中KeyStore類負責金鑰的管理,KeyStore有個setKeyEntry()方法。通用的流程是KeyStore將key設定為一個key entry。然後通過store()方法儲存為.keystore檔案。使用方得到.keystore檔案,利用load()方法讀取Key entry,然後使用。

如果是非對稱金鑰的祕密金鑰,寫入金鑰項的使用方法如下:

public static void secretKeyStore() throws KeyStoreException, NoSuchAlgorithmException,
                    CertificateException, IOException {
        char[] password = "123456".toCharArray();
        String fileName = System.getProperty("user.home") + File.separator + ".keystore";
        FileInputStream fis = new FileInputStream(fileName);
        KeyStore ks = KeyStore.getInstance("jceks");
        ks.load(fis, password);
        KeyGenerator kg = KeyGenerator.getInstance("DES");
        SecretKey key = kg.generateKey();

        ks.setKeyEntry("myKeyEntry", key, password, null);

        FileOutputStream fos = new FileOutputStream(fileName);
        ks.store(fos, password);
        System.out.println("store key in " + fileName);
    }

這裡帶來一些概念:

  • 金鑰庫:也就是上面說的KeyStore,用來管理存放金鑰和證照的地方。Java的金鑰管理是基於金鑰庫來構建的。
  • 金鑰項:金鑰庫裡存放的是一條條的金鑰項。金鑰項要麼儲存一個非對稱金鑰對,要麼儲存一個祕密金鑰。如果儲存的是金鑰對,那還可能儲存一個證照鏈。證照鏈的第一個證照包含公鑰。
  • 別名:每個金鑰都會可以有個別名,可以理解為金鑰項的名字。
  • 標識名:金鑰庫中的實體的標識名是其完整的X.500名的子集,比如一個DN是

    CN=Yu Jia, OU=ALI, O=ALIBABA, L=HZ, ST=ZJ, C=CN
  • 證照項:只包含一個公鑰證照,儲存的是證照而不是證照鏈。
  • JKS,JCEKS,PKCS12:金鑰庫演算法,Java預設是JKS,只能儲存私鑰,要想儲存對稱金鑰的祕密金鑰,需要使用JCEKS,這也就是上面程式碼中提到的KeyStore ks = KeyStore.getInstance("jceks");。可以通過修改java.security檔案中的keystore.type=JCEKS來更改預設演算法。

Keytool

光是這樣,還欠點什麼,因為上面的程式碼放到main函式裡還是無法執行,而且也有個疑問,明明是要建立keystore,幹嘛還要先load?
看看KeyStore中store()方法的原始碼:

public final void store(OutputStream stream, char[] password)
        throws KeyStoreException, IOException, NoSuchAlgorithmException,
            CertificateException
    {
        if (!initialized) {
            throw new KeyStoreException("Uninitialized keystore");
        }
        keyStoreSpi.engineStore(stream, password);
    }

未初始化的Keystore是要丟擲KeyStoreException的。而初始化動作是在load()方法裡做的。那這就奇怪了,第一個keystore難道是自己隨便在系統目錄裡touch的?

這就引出了keytool工具,這是一個JRE提供的管理工具,方便管理金鑰庫的。keytool是命令列介面,使用keytool命令可以管理金鑰庫,具體命令各個引數可以man keytool或者keytool -help瞭解。

我這裡列出我的程式是如何初始化一個keystore的:
1,我先生成了一個別名叫做changedi的金鑰項,其演算法是RSA非對稱演算法

zunyuanjys-MacBook-Air:~ zunyuan.jy$ keytool -genkey -alias changedi -keyalg RSA
輸入金鑰庫口令:  
再次輸入新口令: 
您的名字與姓氏是什麼?
  [Unknown]:  Yu Jia
您的組織單位名稱是什麼?
  [Unknown]:  ALI
您的組織名稱是什麼?
  [Unknown]:  ALIBABA
您所在的城市或區域名稱是什麼?
  [Unknown]:  HZ
您所在的省/市/自治區名稱是什麼?
  [Unknown]:  ZJ
該單位的雙字母國家/地區程式碼是什麼?
  [Unknown]:  CN
CN=Yu Jia, OU=ALI, O=ALIBABA, L=HZ, ST=ZJ, C=CN是否正確?
  [否]:  Y

輸入 <changedi> 的金鑰口令
    (如果和金鑰庫口令相同, 按回車):  
再次輸入新口令: 

2,依照提示輸入完成DN後,keystore就建立好了,可以檢視一下

zunyuanjys-MacBook-Air:~ zunyuan.jy$ keytool -list
輸入金鑰庫口令:  

金鑰庫型別: JKS
金鑰庫提供方: SUN

您的金鑰庫包含 1 個條目

changedi, 2016-7-7, PrivateKeyEntry, 
證照指紋 (SHA1): 76:C8:CE:EA:4C:29:6D:0E:FF:8C:02:BE:F4:F4:55:97:63:1F:C8:26

3,可以看到,這個庫還是JKS的,需要更改為JCEKS,於是做下面的事

zunyuanjys-MacBook-Air:~ zunyuan.jy$ keytool -keypasswd -alias changedi -storetype jceks
輸入金鑰庫口令:  
輸入 <changedi> 的金鑰口令
新<changedi> 的金鑰口令: 
重新輸入新<changedi> 的金鑰口令: 

4,再list時,要選擇storetype,因為剛才雖然是修改密碼,但是其實核心目的是要更改金鑰庫型別

zunyuanjys-MacBook-Air:~ zunyuan.jy$ keytool -list -storetype jceks
輸入金鑰庫口令:  

金鑰庫型別: JCEKS
金鑰庫提供方: SunJCE

您的金鑰庫包含 1 個條目

changedi, 2016-7-7, PrivateKeyEntry, 
證照指紋 (SHA1): 76:C8:CE:EA:4C:29:6D:0E:FF:8C:02:BE:F4:F4:55:97:63:1F:C8:26

5,執行剛才的程式,寫一個對稱金鑰的祕密金鑰進去,作為這個keystore的一個金鑰項,再list

zunyuanjys-MacBook-Air:~ zunyuan.jy$ keytool -list -storetype jceks
輸入金鑰庫口令:  

金鑰庫型別: JCEKS
金鑰庫提供方: SunJCE

您的金鑰庫包含 2 個條目

changedi, 2016-7-7, PrivateKeyEntry, 
證照指紋 (SHA1): 76:C8:CE:EA:4C:29:6D:0E:FF:8C:02:BE:F4:F4:55:97:63:1F:C8:26
mykeyentry, 2016-7-7, SecretKeyEntry, 

其實上面的例子,在建立第一個金鑰項時就可以指定storetype是JCEKS,我這裡只是展示一下如何切換金鑰庫型別。另外在RSA的私鑰金鑰項在未指定證照的情況下也會生成一個自簽名證照。

回到剛才的程式碼裡,我們看看setKeyEntry的細節:

public final void setKeyEntry(String alias, Key key, char[] password,
                                  Certificate[] chain)
        throws KeyStoreException
    {
        if (!initialized) {
            throw new KeyStoreException("Uninitialized keystore");
        }
        if ((key instanceof PrivateKey) &&
            (chain == null || chain.length == 0)) {
            throw new IllegalArgumentException("Private key must be "
                                               + "accompanied by certificate "
                                               + "chain");
        }
        keyStoreSpi.engineSetKeyEntry(alias, key, password, chain);
    }

可以看到,如果是非對稱金鑰的生產,需要提供一個證照鏈,否則就拋異常。考慮到這樣的情況,我們一般不是做專業的企業級安全。還是keytool搞定好了。


相關文章