iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 8
0
Security

安全地寫 Java 的「基本功」系列 第 8

安全地寫 Java 的 「基本功」- Day 7

AdvancedRSA

延伸昨天所做之 AdvancedAES,今天,我們要用非同步的機制來擴充它。

Test First

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 進行加密。最後,用私鑰解開。

PkiParameter

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,並取出公鑰與私鑰。因此,我們可以有一個存取金鑰的介面。

AsymmCryptoUtils

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(不用告訴我密碼是什麼...)給我,這樣我們就可以安安全全地保證檔案的機密性了。

小結

這個禮拜來,我們從基本的資安概念談到加解密的應用,把機密性的實務應用做了簡單的介紹。希望各位不會覺得太硬,或太簡單。我個人是覺得太簡單,但因為不是寫書或寫論文,無法一天八小實在電腦前深入地探討每個主題,因此必須把握每天僅有的一到兩個小時,做精簡扼要的介紹。

接下來,我們會繼續進入資訊安全其他的領域。但在介紹技術細節前,我們必須先來了解資訊安全的風險與威脅。


上一篇
安全地寫 Java 的 「基本功」- Day 6
下一篇
安全地寫 Java 的 「基本功」- Day 8
系列文
安全地寫 Java 的「基本功」14

尚未有邦友留言

立即登入留言