延伸昨天所做之 AdvancedAES,今天,我們要用非同步的機制來擴充它。
package javaxx.cipher;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static javaxx.cipher.EncryptionParameter.ALGORITHM_AES;
import static javaxx.cipher.EncryptionParameter.STRENGTH_256;
/**
* AdvancedRsaTest
* cchuang, 2016/12/7.
*/
public class AdvancedRsaTest {
private static final String SOURCE_FILE = "/test.png";
private static final String ENCRYPTED_FILE = "test.png.enc";
private static final String DECRYPTED_FILE = "test.png.dec";
private static final String TEST_PASSWORD = "s3cR3T p4SSw@Rd";
private static final String KEY_STORE_FILE = "/JavaKeyStore.jks";
private static final String KEY_STORE_PASSWORD = "1234567890";
private static final String KEY_ALIAS = "myKey";
private EncryptionParameter param;
private File sourceFile;
private File encryptedFile;
private File decryptedFile;
private String masterKey;
private PkiParameter pkiParam;
@Before
public void setUp() throws Exception {
this.masterKey = TEST_PASSWORD;
this.param = initialEncryptionParameter();
this.sourceFile = initFileFromClasspath(SOURCE_FILE);
this.encryptedFile = initEmptyFileBySource(this.sourceFile, ENCRYPTED_FILE);
this.decryptedFile = initEmptyFileBySource(this.sourceFile, DECRYPTED_FILE);
this.pkiParam = initPkiParameter(initFileFromClasspath(KEY_STORE_FILE), KEY_STORE_PASSWORD);
}
private PkiParameter initPkiParameter(File keyStoreFile, String password)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
return PkiParameter.Builder.create()
.setKeyStoreFile(keyStoreFile)
.setPassword(password)
.buildJksByLoading();
}
private EncryptionParameter initialEncryptionParameter() throws NoSuchAlgorithmException, InvalidKeySpecException {
return EncryptionParameter.Builder.create()
.setAlgorithm(ALGORITHM_AES)
.setStrength(STRENGTH_256)
.setPassword(TEST_PASSWORD)
.setSalt(SymmCryptoUtils.genSalt())
.buildForEncryptingByPassword();
}
private File initFileFromClasspath(String fileName) throws URISyntaxException {
File fileInClasspath = new File(this.getClass().getResource(fileName).toURI());
return fileInClasspath;
}
private File initEmptyFileBySource(File exists, String fileName) {
File encryptedFile = exists.toPath()
.getParent()
.resolve(fileName)
.toFile();
return encryptedFile;
}
@Test
public void testAsymmetricEncryption() throws Exception {
SymmCryptoUtils.encryptFile(this.param, this.sourceFile, this.encryptedFile);
byte[] cipher =
AsymmCryptoUtils.encryptData(
this.pkiParam.getPublicKey(KEY_ALIAS, KEY_STORE_PASSWORD),
this.param.getSecretKey().getEncoded());
Assert.assertFalse(Arrays.equals(this.param.getSecretKey().getEncoded(), cipher));
byte[] secretKeyContent =
AsymmCryptoUtils.decryptData(
this.pkiParam.getPrivateKey(KEY_ALIAS, KEY_STORE_PASSWORD),
cipher);
Assert.assertTrue(Arrays.equals(this.param.getSecretKey().getEncoded(), secretKeyContent));
SecretKey secretKey =
new SecretKeySpec(secretKeyContent, 0, secretKeyContent.length, EncryptionParameter.ALGORITHM_AES);
EncryptionParameter decParam =
EncryptionParameter.Builder.create()
.setAlgorithm(EncryptionParameter.ALGORITHM_AES)
.setStrength(EncryptionParameter.STRENGTH_256)
.setSecretKey(secretKey)
.setInitVector(this.param.getInitVector())
.buildForDirectlyInitial();
SymmCryptoUtils.decryptFile(decParam, this.encryptedFile, this.decryptedFile);
checkDecryptedFile();
}
private void checkDecryptedFile() throws IOException {
Assert.assertTrue(this.decryptedFile.exists());
Assert.assertTrue(
Arrays.equals(IOUtils.toByteArray(new FileInputStream(this.sourceFile)),
IOUtils.toByteArray(new FileInputStream(this.decryptedFile))));
}
@After
public void tearDown() throws Exception {
this.encryptedFile.delete();
this.decryptedFile.delete();
}
}
我們用先前所說到的金鑰工具 - keytool,產生了一個 JavaKeyStore.jks 檔案。它裡頭會有一把私鑰,及一把公鑰。因此,我們用公鑰來對檔案加密時,所用的 SecretKey 進行加密。最後,用私鑰解開。
package javaxx.cipher;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import org.apache.commons.lang3.StringUtils;
/**
* PkiParameter
* cchuang, 2016/12/7.
*/
public class PkiParameter {
public static final String TYPE_JKS = "JKS";
private PkiParameter() {}
private KeyStore keyStore;
public KeyStore getKeyStore() {
return keyStore;
}
public Key getKey(String alias, String password)
throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
if (null == keyStore) {
throw new IllegalStateException();
} else if (null == password || StringUtils.isEmpty(alias)) {
throw new IllegalArgumentException();
}
Key key = this.keyStore.getKey(alias, password.toCharArray());
return key;
}
public PublicKey getPublicKey(String alias, String password)
throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
Key key = getKey(alias, password);
if (key instanceof PrivateKey) {
Certificate cert = this.keyStore.getCertificate(alias);
PublicKey pubKey = cert.getPublicKey();
return pubKey;
}
throw new IllegalStateException();
}
public PrivateKey getPrivateKey(String alias, String password)
throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
Key key = getKey(alias, password);
if (key instanceof PrivateKey) {
return (PrivateKey) key;
}
throw new IllegalStateException();
}
public KeyPair getKeyPair(String alias, String password)
throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
Key key = getKey(alias, password);
if (key instanceof PrivateKey) {
Certificate cert = this.keyStore.getCertificate(alias);
PublicKey pubKey = cert.getPublicKey();
KeyPair keyPair = new KeyPair(pubKey, (PrivateKey) key);
return keyPair;
}
throw new IllegalStateException();
}
public static final class Builder {
private KeyStore keyStore;
private String password;
private File keyStoreFile;
private Builder() {}
public static Builder create() { return new Builder();}
public Builder setKeyStore(KeyStore keyStore) {
this.keyStore = keyStore;
return this;
}
public Builder setKeyStoreFile(File keyStoreFile) {
this.keyStoreFile = keyStoreFile;
return this;
}
public Builder setPassword(String password) {
this.password = password;
return this;
}
public PkiParameter buildDirectly() {
if (null == this.keyStore) {
throw new IllegalArgumentException();
}
PkiParameter pkiParameter = new PkiParameter();
pkiParameter.keyStore = this.keyStore;
return pkiParameter;
}
public PkiParameter buildJksByLoading() throws KeyStoreException, CertificateException,
NoSuchAlgorithmException, IOException {
if (!isLoadingFromFile()) {
throw new IllegalArgumentException();
}
try (FileInputStream fis = new FileInputStream(keyStoreFile)) {
keyStore = KeyStore.getInstance(TYPE_JKS);
keyStore.load(fis, password.toCharArray());
return buildDirectly();
}
}
private boolean isLoadingFromFile() {
return null != keyStoreFile && keyStoreFile.exists() && null != password;
}
}
}
PkiParameter 的工作,主要在於載入 KeyStore,並取出公鑰與私鑰。因此,我們可以有一個存取金鑰的介面。
package javaxx.cipher;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.apache.commons.lang3.ArrayUtils;
/**
* AsymmCryptoUtils
* cchuang, 2016/12/8.
*/
public class AsymmCryptoUtils {
private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
public static byte[] encryptData(PublicKey pubKey, byte[] plainContent)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException {
if (null == pubKey || ArrayUtils.isEmpty(plainContent)) {
throw new IllegalArgumentException();
}
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
byte[] cipherContent = cipher.doFinal(plainContent);
return cipherContent;
}
public static byte[] decryptData(PrivateKey privKey, byte[] cipherContent)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException {
if (null == privKey || ArrayUtils.isEmpty(cipherContent)) {
throw new IllegalArgumentException();
}
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privKey);
byte[] plainContent = cipher.doFinal(cipherContent);
return plainContent;
}
}
這個非同步加解密的程式,僅考量了最簡單的加解密機制,因為更複雜的,你可以在 Google 上找到分區塊加解密的方式。然而在 RSA 加解密應用中。我們很少會用它來處理資料量太大的對象,因此只要能進行加解密,反過來,我們就能進行簽章與驗簽的作用。
在整個程式裡,你會發現,其實最沒有安全性的,就是初始向量(InitialVector)與公鑰(PublicKey),換句話說,你可以告訴你的朋友,我把我的公鑰給你,然後你用密碼加密後,把加密的檔案、原封不動的初始向量、用我的公鑰加密後的 SecretKey(不用告訴我密碼是什麼...)給我,這樣我們就可以安安全全地保證檔案的機密性了。
這個禮拜來,我們從基本的資安概念談到加解密的應用,把機密性的實務應用做了簡單的介紹。希望各位不會覺得太硬,或太簡單。我個人是覺得太簡單,但因為不是寫書或寫論文,無法一天八小實在電腦前深入地探討每個主題,因此必須把握每天僅有的一到兩個小時,做精簡扼要的介紹。
接下來,我們會繼續進入資訊安全其他的領域。但在介紹技術細節前,我們必須先來了解資訊安全的風險與威脅。