前言
出于安全考慮,數(shù)據(jù)庫部分表字段數(shù)據(jù)是通過AES方式進(jìn)行加密存儲(chǔ)的,但是運(yùn)維層面有時(shí)候又需要查詢相關(guān)字段的明文數(shù)據(jù),本篇文章將針對這類場景,給一下相關(guān)的處理方案。
前置知識(shí)點(diǎn)
講AES解密之前,我們有必要先來認(rèn)識(shí)一下什么是AES加解密算法。
這里我們舉一個(gè)常見的例子:AES-128/ECB/PKCS5Padding,這是一種加密算法的描述,它包含了三個(gè)主要部分:加密算法(AES-128)、工作模式(ECB)和填充方式(PKCS5Padding)。以下是對這三個(gè)部分的詳細(xì)解釋:
1. AES-128
AES(Advanced Encryption Standard,高級(jí)加密標(biāo)準(zhǔn))是一種對稱加密算法,廣泛用于保護(hù)電子數(shù)據(jù)的安全性。AES 支持三種密鑰長度:
- AES-128:使用 128 位(16 字節(jié))密鑰。
- AES-192:使用 192 位(24 字節(jié))密鑰。
- AES-256:使用 256 位(32 字節(jié))密鑰。
AES-128 是其中最常用的一種,因?yàn)樗峁┝俗銐虻陌踩?,同時(shí)計(jì)算效率較高。AES 是一種分組密碼,它將數(shù)據(jù)分成固定大小的塊(通常是 128 位)進(jìn)行加密。
2. ECB(Electronic Codebook Mode)
ECB 是 AES 的一種工作模式,用于處理分組密碼。在 ECB 模式下,每個(gè)數(shù)據(jù)塊獨(dú)立加密,相同的明文塊會(huì)被加密成相同的密文塊。ECB 模式的特點(diǎn)如下:
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,加密速度快。
缺點(diǎn):安全性較低。如果明文中存在重復(fù)的模式,這些模式會(huì)在密文中顯現(xiàn)出來,從而可能被攻擊者利用。不適合加密大量數(shù)據(jù)或包含重復(fù)模式的數(shù)據(jù)。
- 示例:
假設(shè)明文為Hello, World!Hello, World!,使用ECB模式加密后,兩個(gè)Hello, World!會(huì)被加密成相同的密文塊。
PS:AES支持的工作模式當(dāng)然不止只有ECB,比較常用的工作模式還有:CBC、CTR、GCM ,需要注意的是ECB工作模式算是所有工作模式中比較簡單的一種,只需要基本的明文+秘鑰就能工作,而其他工作模式還需要額外提供初始化向量IV或者計(jì)數(shù)器來實(shí)現(xiàn)加密算法的復(fù)雜性和安全性。
3. PKCS5Padding(填充方式)
AES 是一種分組密碼,要求明文的長度必須是塊大小的整數(shù)倍(AES 的塊大小為 128 位,即 16 字節(jié))。如果明文長度不足,需要進(jìn)行填充。PKCS5Padding 是一種常用的填充方式,它通過在明文末尾添加特定的字節(jié)來確保明文長度符合塊大小的要求。
- 填充規(guī)則:
如果明文長度已經(jīng)是塊大小的整數(shù)倍,則添加一個(gè)完整的填充塊。
填充的字節(jié)值等于填充的字節(jié)數(shù)。例如:
如果需要填充 3 個(gè)字節(jié),則填充為0x03 0x03 0x03。
如果需要填充 16 個(gè)字節(jié),則填充為0x10 0x10 0x10 ... 0x10。 - 示例:
假設(shè)明文為Hello, World!,長度為 13 字節(jié),不足 16 字節(jié)。使用 PKCS5Padding 填充后,明文變?yōu)椋?code>Hello, World!0x03 0x03 0x03
(一)方案一:使用Oracle的DBMS_CRYPTO包函數(shù)
基于AES-128/ECB/PKCS5Padding算法的解密
我們無法直接在select 語句中直接使用DBMS_CRYPTO包的函數(shù),所以我們需要額外創(chuàng)建一個(gè)自定義函數(shù)來滿足我們的需要:
CREATE OR REPLACE FUNCTION TMP_USER_DECRYPTED_FUNC(
p_encrypted_data IN VARCHAR2,
p_key IN VARCHAR2
) RETURN VARCHAR2 IS
v_decrypted_data RAW(2000);
v_decrypted_string VARCHAR2(4000);
BEGIN
-- 解密操作
v_decrypted_data := DBMS_CRYPTO.DECRYPT(
src => utl_raw.cast_to_raw(p_encrypted_data),
typ => DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_ECB + DBMS_CRYPTO.PAD_PKCS5,
key => UTL_I18N.STRING_TO_RAW(p_key, 'AL32UTF8')
);
-- 將解密后的 RAW 數(shù)據(jù)轉(zhuǎn)換為字符串
v_decrypted_string := UTL_I18N.RAW_TO_CHAR(v_decrypted_data, 'AL32UTF8');
RETURN v_decrypted_string;
END;
這里我們對上面的函數(shù)做一下簡單的介紹
①定義函數(shù)名為:TMP_USER_DECRYPTED_FUNC,定義了函數(shù)的入?yún)⒁还灿?個(gè),分別是p_encrypted_data(待解密的數(shù)據(jù))和p_key(秘鑰)
② 聲明了函數(shù)使用到的2個(gè)變量v_decrypted_data和v_decrypted_string
③ 函數(shù)內(nèi)容使用了DBMS_CRYPTO包下的DECRYPT函數(shù),傳入轉(zhuǎn)換為raw類型的待解密數(shù)據(jù),解密類型以及秘鑰三個(gè)參數(shù)后,講最終的結(jié)果轉(zhuǎn)換為字符串類型的格式賦值給v_decrypted_string變量并返回
如果自定義函數(shù)編譯失敗,可能是由于相關(guān)的函數(shù)包沒有權(quán)限導(dǎo)致的,需要執(zhí)行下面的語句進(jìn)行授權(quán):
GRANT EXECUTE ON DBMS_CRYPTO TO 用戶名;
GRANT EXECUTE ON UTL_I18N TO 用戶名;
注意AES加密過的數(shù)據(jù)由于數(shù)據(jù)編碼的原因常常會(huì)伴隨著進(jìn)行base64編碼,所以如果數(shù)據(jù)同樣也被進(jìn)行base64編碼的話,則可以參考下面的函數(shù):
CREATE OR REPLACE FUNCTION TMP_USER_DECRYPTED_FUNC(
p_encrypted_data IN VARCHAR2,
p_key IN VARCHAR2
) RETURN VARCHAR2 IS
v_decrypted_data RAW(2000);
v_decrypted_string VARCHAR2(4000);
BEGIN
-- 解密操作
v_decrypted_data := DBMS_CRYPTO.DECRYPT(
src => utl_encode.base64_decode(utl_raw.cast_to_raw(p_encrypted_data)),
typ => DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_ECB + DBMS_CRYPTO.PAD_PKCS5,
key => UTL_I18N.STRING_TO_RAW(p_key, 'AL32UTF8')
);
-- 將解密后的 RAW 數(shù)據(jù)轉(zhuǎn)換為字符串
v_decrypted_string := UTL_I18N.RAW_TO_CHAR(v_decrypted_data, 'AL32UTF8');
RETURN v_decrypted_string;
END;
自定義函數(shù)創(chuàng)建完成后,我們就可以在select語句中使用了
SELECT TMP_USER_DECRYPTED_FUNC(column ,'秘鑰') AS decrypted_text FROM TABLE_NAME;
基于AES-128/CBC/PKCS5Padding算法的解密
CBC算是比較常見的AES算法的工作模式,所以這里也單獨(dú)拿出來介紹一下,和ECB的解密過程很類似,CBC工作模式無非只是需要多傳入初始化向量IV作為入?yún)⒍?/p>
CREATE OR REPLACE FUNCTION TMP_USER_DECRYPTED_FUNC(
p_encrypted_data IN VARCHAR2,
p_key IN VARCHAR2,
p_iv IN VARCHAR2
) RETURN VARCHAR2 IS
v_decrypted_data RAW(2000);
v_decrypted_string VARCHAR2(4000);
BEGIN
-- 解密操作
v_decrypted_data := DBMS_CRYPTO.DECRYPT(
src => utl_raw.cast_to_raw(p_encrypted_data),
typ => DBMS_CRYPTO.ENCRYPT_AES128 + DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5,
key => UTL_I18N.STRING_TO_RAW(p_key, 'AL32UTF8'),
iv => UTL_I18N.STRING_TO_RAW(p_iv, 'AL32UTF8')
);
-- 將解密后的 RAW 數(shù)據(jù)轉(zhuǎn)換為字符串
v_decrypted_string := UTL_I18N.RAW_TO_CHAR(v_decrypted_data, 'AL32UTF8');
RETURN v_decrypted_string;
END;
(二)方案二:借助Linux的解密命令來完成解密
如果數(shù)據(jù)量不大的話,其實(shí)我們也可以先把數(shù)據(jù)提取成csv文件出來,我們借助Linux系統(tǒng)的openssl命令直接解密就行
下面我們來對腳本的內(nèi)容做一下簡單的介紹:
- ①定義待解密的文件、解密后的輸出文件以及秘鑰,注意由于秘鑰需要用16 字節(jié),32 個(gè)十六進(jìn)制的字符來表示,所以這里我們主要使用
od命令來完成明文秘鑰的轉(zhuǎn)換 - ②逐行讀取待解密的源文件,先使用
base64命令進(jìn)行文本的base64解碼,再將解碼后的內(nèi)容使用openssl命令來進(jìn)行解密,這里我們通過-aes-128-ecb參數(shù)指定了解密方式,-K參數(shù)指定了秘鑰,-nopad指定不需要數(shù)據(jù)填充 - ③將解密后文本放入輸出文件中
#!/bin/bash
# 輸入文件和輸出文件
input_file="aes_source_file.csv"
output_file="aes_target_file.csv"
aes_key_hex=$(echo -n "明文字符串秘鑰" | od -A n -t x1 | tr -d ' \n')
# 清空輸出文件
> "$output_file"
# 逐行讀取加密文件
beginTime=`date +%s`
while IFS= read -r line; do
# Base64 解碼
decoded=$(echo -n "$line" | tr -d '\n' | base64 --decode)
# AES 解密,使用字節(jié)形式的密鑰
plaintext=$(echo -n "$decoded" | openssl enc -d -aes-128-ecb -K "$aes_key_hex" -nopad)
# 將解密后的內(nèi)容寫入輸出文件
echo "$plaintext" >> "$output_file"
done < "$input_file"
endTime=`date +%s`
difference=$((endTime - beginTime))
echo "執(zhí)行完畢,耗時(shí) ${difference} 秒"
echo "解密完成,結(jié)果已保存到 $output_file"
需要注意的是,方案二根據(jù)筆者自己驗(yàn)證,處理的速度比較依賴服務(wù)器的cpu性能,在8核24G的服務(wù)器下單線程處理速度大概是每秒60+條數(shù)據(jù),在不拆分?jǐn)?shù)據(jù)并發(fā)處理的前提下不太使用來用跑大批量的數(shù)據(jù)。超過100w的數(shù)據(jù)建議還是換種方式來處理
(三)方案三:用其他語言來實(shí)現(xiàn)
個(gè)人是傾向于問題能簡單解決,就不要上難度,但是實(shí)際操作中數(shù)據(jù)庫函數(shù)包的授權(quán)你不一定拿得到,而方案二也不太適合用來處理大數(shù)據(jù)量的數(shù)據(jù),那么方案三就是必能解決問題的最終方案,事實(shí)上大部分問題都可以用代碼來解決,已經(jīng)有無數(shù)前輩把函數(shù)封裝成依賴提供出來我們拿來用就行。
這里我們簡單列舉一下java的核心代碼
// 解密
public static String Decrypt(String sSrc, String sKey) throws Exception {
try {
// 判斷Key是否正確
if (sKey == null) {
System.out.print("Key為空null");
return null;
}
// 判斷Key是否為16位
if (sKey.length() != 16) {
System.out.print("Key長度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = Base64.getDecoder().decode(sSrc);//先用base64解密
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original,"utf-8");
return originalString;
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
} catch (Exception ex) {
System.out.println(ex.toString());
return null;
}
}