跳至主要内容

Java AES 加解密

  • 有時會需要將資料進行可回復的加密。
  • 例如: ISO 27001 DLP(data leakage prevention),一般在驗證時最低要求為密碼原則、硬碟資料與 DB 資料加密。
  • 此時若牽涉到敏感性資料,下列方法也許有用。

DBA 知道加密時的 half-salt。 DBA 只知道一半的 salt 且有機會接觸密文。無法完整解密。 工程師知道加密演算法加另一半的 salt 以及 salt 合併方法。但是無接觸資料。 App 使用者僅能在系統上使用資料。

AES 進階加密標準

  • 對稱加密演算法 :AES算法使用 [相同] 的密鑰來對資料進行 [加密][解密]

加密種類: 依加解密演算法種類分 對稱式非對稱式 加密

對稱性加密算法:

對稱式加密就是加密和解密使用同一個密鑰 資料接收雙方需先知道雙方加密的金鑰與演算法,才能完整加解密。 ex: AES、DES、3DES

非對稱算法:

非對稱式加密就是加密和解密所使用不同密鑰,一般稱 公鑰私鑰 。 公私鑰必需配對使用,否則不能打開加密文件。

訊息接收方先製備公鑰與私鑰。並將公鑰發送給資料來源方。 資料源頭則使用接收方的公鑰將訊息加密,然後將祕文回傳給接收方,接收方再依據自己規則結合公私鑰進行解密。

所以,原則上是 接收 訊息的人來 決定演算 工具和 派發金鑰 。 ex: RSA、DSA、ECC

Aes128 Encryption and Decryption Example

  • Aes128EncrypterUtil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Aes128EncrypterUtil { public static String encryptHex(String str) { Aes128Cipher encrypter = new Aes128Cipher(); return encrypter.encrypt(str); } public static String decryptHex(String str) { Aes128Cipher encrypter = new Aes128Cipher(); return encrypter.decrypt(str); } public static String encryptBased64(String str) { Aes128Cipher encrypter = new Aes128Cipher(); return encrypter.encrypt(str, "BASED64"); } public static String decryptBased64(String str) { Aes128Cipher encrypter = new Aes128Cipher(); return encrypter.decrypt(str, "BASED64"); } }
  • Aes128Cipher.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; /** * <pre> <h1>進階加密標準</h1> <h2>對稱加密算法 :</h2> AES算法使用[相同]的密鑰來對資料進行[加]密和[解]密 <h2>加密過程包括以下步驟:</h2> ref: https://inbound.technology/%E4%BB%80%E9%BA%BC%E6%98%AF-aes-%E5%B0%8D%E7%A8%B1%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95%EF%BC%9Fphp-%E8%88%87-nodejs-%E7%AF%84%E4%BE%8B%E5%AF%A6%E4%BD%9C/ <ol> <li>密鑰擴展:將輸入的密鑰擴展為更長的密鑰序列,以供後續的加密和解密過程使用。</li> <li>初始輪:將明文數據按照一定的規則與密鑰序列進行異或操作。</li> <li>輪函數:重覆執行多輪操作,每一輪都包括四個步驟:</li> <li>字節替換:將數據中的每個字節替換為另一個字節,使用一個固定的S盒進行映射。</li> <li>行移位:將數據矩陣中的每一行循環左移不同的偏移量。</li> <li>列混淆:將數據矩陣中的每一列進行混淆。</li> <li>密鑰加:將密鑰序列中的一部分與數據進行異或操作。</li> <li>最終輪:最後一輪操作中,省略列混淆步驟,只包括字節替換、行移位和密鑰加。</li> <li>輸出:輸出加密後的數據。</li> </ol> * </pre> */ public class Aes128Cipher { public static final String CIPHER_TO_BASED64 = "BASED64"; public static final String CIPHER_TO_HEX = "HEX"; private Cipher ecipher; private Cipher dcipher; private final byte[] keySalt = new byte[] { 45, -42, 105, -110, 115, -101, 99, -116, 45, -116, -111, 116, -101, 109, 42, -45 }; private static final String ENCODING_UTF8 = "UTF8"; public static void main(String[] args) { KeyGenerator kg = null; try { kg = KeyGenerator.getInstance("AES"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } kg.init(128); SecretKey secretKey = kg.generateKey(); byte[] encodedByteArray = secretKey.getEncoded(); System.out.println(Arrays.toString(encodedByteArray)); } public Aes128Cipher() throws EncryptDecryptException { SecretKeySpec skeySpec = new SecretKeySpec(keySalt, "AES"); try { ecipher = Cipher.getInstance("AES"); dcipher = Cipher.getInstance("AES"); ecipher.init(Cipher.ENCRYPT_MODE, skeySpec); dcipher.init(Cipher.DECRYPT_MODE, skeySpec); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) { e.printStackTrace(); throw new EncryptDecryptException("unexpected", e); } } public String encrypt(String str) throws EncryptDecryptException { return encrypt(str, CIPHER_TO_HEX); } public String encrypt(String str, String cipherTo) throws EncryptDecryptException { try { // Encode the string into bytes using utf-8 byte[] utf8 = str.getBytes(ENCODING_UTF8); // Encrypt byte[] enc = ecipher.doFinal(utf8); if (CIPHER_TO_BASED64.equalsIgnoreCase(cipherTo)) { Base64.Encoder encoder = Base64.getEncoder(); return encoder.encodeToString(enc); } else { // encrypt should produce url safe string, which best in HEX return HexStringUtil.toHexString(enc); } } catch (javax.crypto.BadPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) { e.printStackTrace(); throw new EncryptDecryptException("unexpected", e); } } public String decrypt(String str) throws EncryptDecryptException { return decrypt(str, CIPHER_TO_HEX); } public String decrypt(String str, String cypherTo) throws EncryptDecryptException { try { if (CIPHER_TO_BASED64.equalsIgnoreCase(cypherTo)) { Base64.Decoder decoder = Base64.getDecoder(); byte[] cipherText = decoder.decode(str.getBytes("UTF8")); return new String(dcipher.doFinal(cipherText), "UTF-8"); } else { // Decode base64 to get bytes byte[] dec = HexStringUtil.fromHexString(str); // Decrypt byte[] utf8 = dcipher.doFinal(dec); // Decode using utf-8 return new String(utf8, ENCODING_UTF8); } } catch (javax.crypto.BadPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) { e.printStackTrace(); throw new EncryptDecryptException("unexpected", e); } } private static class HexStringUtil { // table to convert a nibble to a hex char. private static char[] hexChar = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** * Convert a hex string to a byte array. Permits upper or lower case * hex. * * @param s * String must have even number of characters. and be formed * only of digits 0-9 A-F or a-f. No spaces, minus or plus * signs. * @return corresponding byte array. */ public static byte[] fromHexString(String s) { int stringLength = s.length(); if ((stringLength & 0x1) != 0) { throw new IllegalArgumentException( "fromHexString requires an even number of hex characters"); } byte[] b = new byte[stringLength / 2]; for (int i = 0, j = 0; i < stringLength; i += 2, j++) { int high = charToNibble(s.charAt(i)); int low = charToNibble(s.charAt(i + 1)); b[j] = (byte) ((high << 4) | low); } return b; } /** * convert a single char to corresponding nibble. * * @param c * char to convert. must be 0-9 a-f A-F, no spaces, plus or * minus signs. * * @return corresponding integer */ private static int charToNibble(char c) { if ('0' <= c && c <= '9') { return c - '0'; } else if ('a' <= c && c <= 'f') { return c - 'a' + 0xa; } else if ('A' <= c && c <= 'F') { return c - 'A' + 0xa; } else { throw new IllegalArgumentException( "Invalid hex character: " + c); } } public static String toHexString(byte[] b) { StringBuffer sb = new StringBuffer(b.length * 2); for (int i = 0; i < b.length; i++) { // look up high nibble char sb.append(hexChar[(b[i] & 0xf0) >>> 4]); // look up low nibble char sb.append(hexChar[b[i] & 0x0f]); } return sb.toString(); } } }

Trouble shooting : InvalidKeyException

以 256 bits 的 key 建立 AES 256 cipher 時 可能會出現 InvalidKeyException:Illegal key size or default parameters 錯誤 這是 JDK 對加密金鑰長度的限制所致。

  • 下列 Code 可能拋出 InvalidKeyException 1 2 3 4 cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec);

解決 InvalidKeyException 異常方式

JAVA_HOME/jre/lib/security/java.scurity 找出或增加 crypto.policy=unlimited 並設定為 unlimited 然後重啟 Java 即可解決