AndroidKeystore密鑰庫系統,保證本地加密算法的密鑰安全性

1、AndroidKeystore密鑰庫系統介紹

AndroidKeystore系統是一個密鑰庫管理系統,谷歌設計這個系統的初衷應該是爲了對標蘋果的鑰匙串KeyChain,有意思的是谷歌在Android4.0(API14)時便引入了KeyChain,但是並未提供詳盡的說明文檔,僅僅提供了一個API文檔,網上也沒有什麼對它的文章,着實讓人費解。話不多說,有興趣的同學可以自行搜索,下面我來給大家介紹一下今天的主角——AndroidKeystore

谷歌在Android4.3(API18)中引入了AndroidKeystore,在Android6.0(API23)中對AndroidKeystore進行了一波升級優化,目前來說應該是最安全的加解密方案。根據官方文檔說明,原文對它的介紹如下:

利用 Android Keystore 系統,您可以在容器中存儲加密密鑰,從而提高從設備中提取密鑰的難度。在密鑰進入 Keystore 後,可以將它們用於加密操作,而密鑰材料仍不可導出。此外,它提供了密鑰使用的時間和方式限制措施,例如要求進行用戶身份驗證才能使用密鑰,或者限制爲只能在某些加密模式中使用。

乍一看,這似乎是一個密鑰保險箱,安全性高,可以把加密密鑰存入其中,需要的時候再通過它用我們的密鑰。**可是!!這貨的使用根本就不是個存儲空間!!**我們先來看看這東西大致的使用流程是怎樣的,讓大家直觀的明白它的作用:

// step 1  通過密鑰別名判斷是否已有密鑰
keyStore.containsAlias(keyAlias);

// step 2  沒有密鑰,生成一個
spec = new KeyGenParameterSpec.Builder(keyAlias, ...)...;
keyPairGenerator.initialize(spec);
keyPairGenerator.generateKeyPair();

// step 3  取出公鑰,進行加密
publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
cipher.doFinal(data);

// step 4  取出私鑰,進行解密
privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
cipher.init(Cipher.DECRYPT_MODE, privateKey);
cipher.doFinal(data);

如此看來,AndroidKeystore根本就不是個密鑰存儲庫。這和EncryptUtil這樣的工具類幾乎是一樣的,先init,再encrypt,然後decrypt,儼然一個加解密工具。說白了,它在內部生成一個密鑰,我們可以用它來加解密數據,並不能把我們自己生成的密鑰送進去。

根據文檔的介紹,它的大部分功能都只在Android6.0(API23)及以上支持,最低支持的Android4.3(API18)只提供RSA/ECB算法的使用。

2、密鑰的檢測與生成

AndroidKeystore是通過密鑰的別名來進行加解密的,那麼我們在使用之前必然是需要判斷一下密鑰是否存在的,AndroidKeystore提供了containsAlias()方法來完成密鑰的檢測,具體使用如下:

public boolean hasKey(String keyAlias) {
	try {
		// 加載一個AndroidKeyStore類型的KeyStore,貌似是定死的類型。
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		return keyStore.containsAlias(keyAlias);
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
	return false;
}

這裏可以"AndroidKeyStore"用常量存一下,後面還會用到,避免魔法字符串。關於字符串常量我就不單獨提出來了,不方便閱讀,接下來有使用的字符串都建議在項目中提取爲字符串常量。

密鑰的檢測很簡單,我就不贅述了,這裏不得不吐槽一句,異常真多,當然後面也是一樣的多異常,總讓人用起來不踏實的感覺。

如果密鑰庫中沒有該別名的密鑰,那麼我們得讓它生成一個,重頭戲來了,敲黑板!!!

public void buildKey(Context context, String keyAlias) {
	try{
		// 先獲取密鑰對生成器,採用RSA算法,AndroidKeyStore類型
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
		// 加密算法的相關參數
		AlgorithmParameterSpec spec;
		// 密鑰的有效起止時間,從現在到999年後,時間大家自己定
		Calendar start = Calendar.getInstance();
		Calendar end = Calendar.getInstance();
		end.add(Calendar.YEAR, 999)
		// 生成加密參數,從Android6.0(API23)開始有所不同
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			// 根據密鑰別名生成加密參數,提供加密和解密操作
			spec = new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
				// SHA (Secure Hash Algorithm,譯作安全散列算法) 是美國國家安全局 (NSA) 設計,美國國家標準與技術研究院 (NIST) 發佈的一系列密碼散列函數。 感興趣的同學可以瞭解一下
				.setDigests(KeyProperties.DIGEST_SHA512)
				// 填充模式,一般RSA加密常用PKCS1Padding模式
				.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
				// 限定密鑰有效期起止時間
				.setCertificateNotBefore(start.getTime())
				.setCertificateNotAfter(end.getTime())
				.build();
		} else {
			// 相對於Android6.0(API23)的方式,這種稍顯簡單
			spec = new KeyPairGeneratorSpec.Builder(context.getApplicationContext())
				.setAlias(keyAlias)
				// 設置用於生成的密鑰對的自簽名證書的主題,X500Principal這東西不認識,資料真少,看的頭大
				.setSubject(new X500Principal("CN=" + keyAlias))
				// 設置用於生成的密鑰對的自簽名證書的序列號,從BigInteger取即可
				.setSerialNumber(BigInteger.TEN)
				// 限定密鑰有效期起止時間
				.setStartDate(start.getTime())
				.setEndDate(end.getTime())
				.build()
		}
		// 用加密參數初始化密鑰對生成器,生成密鑰對
		keyPairGenerator.initialize(spec);
		keyPairGenerator.generateKeyPair();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (NoSuchProviderException e) {
		e.printStackTrace();
	} catch (InvalidAlgorithmParameterException e) {
		e.printStackTrace();
	}
}

話不多說,都在代碼註釋裏了,有沒講清楚的地方,歡迎評論區指出。

3、加解密流程

來吧,進入令人顫抖的加解密流程,真的,當我寫完我都不可思議。

加解密的流程非常相似,先加載KeyStore,再拿到公鑰/私鑰,最後進行加密/解密。代碼實現也基本一致,那麼問題來了,令人顫抖的是什麼呢?亮代碼:

// 加密方法
public byte[] encrypt(Context context, String keyAlias, byte[] data) {
	if(!hasKey(keyAlias)) {
		buildKey(context, keyAlias);
	}
	try {
		// 獲取"AndroidKeyStore"類型的KeyStore,加載
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		// 拿到密鑰別名對應的Entry
		KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
		if (entry instanceof KeyStore.PrivateKeyEntry) {
			// 通過Entry拿到公鑰對象(並不是真實的公鑰,僅供加密方法使用)
			PublicKey publicKey = ((KeyStore.PrivateKeyEntry)entry).getCertificate().getPublicKey();
			// 使用"RSA/ECB/PKCS1Padding"模式進行加密
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
			return cipher.doFinal(data);
		}
	} catch (IOException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (UnrecoverableEntryException e) {
		e.printStackTrace();
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (NoSuchPaddingException e) {
		e.printStackTrace();
	} catch (InvalidKeyException e) {
		e.printStackTrace();
	} catch (BadPaddingException e) {
		e.printStackTrace();
	} catch (IllegalBlockSizeException e) {
		e.printStackTrace();
	}
	return null;
}


// 解密方法
public byte[] decrypt(Context context, String keyAlias, byte[] data) {
	if(!hasKey(keyAlias)) {
		buildKey(context, keyAlias);
	}
	try {
		// 獲取"AndroidKeyStore"類型的KeyStore,加載
		KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		keyStore.load(null);
		// 拿到密鑰別名對應的Entry
		KeyStore.Entry entry = keyStore.getEntry(keyAlias, null);
		if (entry instanceof KeyStore.PrivateKeyEntry) {
			// 通過Entry拿到私鑰對象(並不是真實的私鑰,僅供解密方法使用)
			PrivateKey privateKey = ((KeyStore.PrivateKeyEntry)entry).getPrivateKey();
			// 使用"RSA/ECB/PKCS1Padding"模式進行解密
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			return cipher.doFinal(data);
		}
	} catch (IOException e) {
		e.printStackTrace();
	} catch (CertificateException e) {
		e.printStackTrace();
	} catch (NoSuchAlgorithmException e) {
		e.printStackTrace();
	} catch (UnrecoverableEntryException e) {
		e.printStackTrace();
	} catch (KeyStoreException e) {
		e.printStackTrace();
	} catch (NoSuchPaddingException e) {
		e.printStackTrace();
	} catch (InvalidKeyException e) {
		e.printStackTrace();
	} catch (BadPaddingException e) {
		e.printStackTrace();
	} catch (IllegalBlockSizeException e) {
		e.printStackTrace();
	}
	return null;
}

謎題揭曉,令人顫抖的異常!!這是我至今爲止,捕獲的最多的異常了,果真是頭髮短見識短,疫情讓我的頭髮長長了,見識的增長也接踵而來。

4、常規用法

通常只把AndroidKeystore用於對密鑰的加密,也就是說,建議將需要加密的數據使用對稱加密算法(如AES)進行加密,而對稱加密算法的密鑰則由AndroidKeystore進行加密保護。通常用法如下:

// saveKey()方法用於將加密後的aesKey持久化,parseByte2Hex()方法用於將二進制轉爲十六進制,防止byte[]轉String導致格式出錯的問題。 
saveKey(parseByte2Hex(encrypt(context, keyAlias, aesKey)));

// readKey()方法用於讀取持久化的加密的aesKey,parseHex2Byte()方法用於將十六進制轉爲二進制,防止String轉byte[]導致格式出錯的問題。 
aesKey = decrypt(context, key, parseHex2Byte(readKey()));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章