使用數字簽名實現資料庫記錄防篡改(Java實現)
文章目錄
一、提出問題
最近在做一個專案,需要對一個現成的產品的資料庫進行操作,增加額外的功能。為此,需要對該產品對資料庫有什麼操作進行研究(至於怎麼監控一個產品的操作會引發什麼資料庫操作,以後會詳細解說)。本來已經對資料庫的操作了如指掌的,無意中發現數據庫表裡的每條記錄都會有這樣一個欄位:
這感覺不妙了,欄位名叫signature,顧名思義,就是簽名的意思呀。難道資料庫表中的每條記錄都會有簽名?也就是說如果我不能正確生成簽名,而直接改記錄中的欄位,會被程式認為非法篡改了資料?那以後我的產品設計,是否也可採用這種方式來對每條記錄做簽名,防止資料被非法篡改,例如日誌表中的資料?抱著這一發現以及這一連串的問題,我進行了以下的研究。在這裡我將研究整理了一下,分享給大家。
二、數字簽名
要解決上面的問題,首先就要對最基礎的知識進行了解。這裡最基礎的知識,無疑就是什麼是數字簽名了。很多同學可能對這個名詞並不陌生,但估計大多數人都是對其一知半解,會把雜湊、非對稱加密、數字簽名、數字證書的幾個概念混為一談,造成混亂。所以我先對相關概念進行解釋,再往下講。如果很熟悉這方面的同學可以跳過此部分,但對於絕大多數同學來說,不建議這樣做。基礎沒搭好,直接看怎麼實現,換了個說法又不知道怎麼去做了。要想提高個人能力,做到舉一反三很重要。
言歸正傳,先對跟數字簽名有關的密碼學知識簡單說一下。加密方法分兩大類,分別是單鑰加密和雙鑰加密,數字簽名涉及到雙鑰加密。關於雙鑰加密,主要涉及到以下幾個要點[1]:
- 雙鑰加密的金鑰有兩把,一把是公開的公鑰,一把是不公開的私鑰
- 公鑰和私鑰是一一對應的關係,有一把公鑰就必然有一把與之對應的、獨一無二的私鑰,反之亦成立。
所有的(公鑰, 私鑰)對都是不同的。 - 用公鑰可以解開私鑰加密的資訊,反之亦成立。
- 同時生成公鑰和私鑰應該相對比較容易,但是從公鑰推算出私鑰,應該是很困難或者是不可能的。
- 在雙鑰體系中,公鑰用來加密資訊,私鑰用來數字簽名。
- 還有一點關於數字證書的。因為任何人都可以生成自己的公鑰私鑰對,所以為了防止有人散佈偽造的騙取信任,就需要一個可靠的第三方機構來生成經過認證的公鑰、私鑰對。簡單來說,數字證書是權威的第三方機構頒發的,用來認證某對公鑰私鑰的證書,經過這個數字證書認證的公鑰私鑰,就可以明確屬於某人或者某機構,是合法的,可信任的。就如同身份證,是證明你身份的一個證件。所以數字證書跟數字簽名是兩回事,要分清楚。
想了解更詳細的數字證書相關內容,可以訪問此地址:http://www.youdzone.com/signature.html。裡面解釋得很形象,應該一看就明白的了。
三、實現步驟
看到這裡,開篇提出的問題也就呼之欲出了。沒錯,就是使用數字簽名技術,將資料庫中的重要欄位進行簽名,將簽名結果作為記錄的一列存在記錄中。這樣當有人入侵資料庫,惡意修改欄位,程式讀資料時拿簽名校驗一下,就知道資料是否有被修改過了。
在java.security包中,有很多有用的類,用以進行安全機制的開發。對於要建立數字簽名,我們主要用到以下的介面或類:
介面名 | 描述 |
---|---|
PrivateKey | A private key |
PublicKey | A public key |
介面:
類名 | 描述 |
---|---|
Signature | The Signature class is used to provide applications the functionality of a digital signature algorithm. |
KeyPair | This class is a simple holder for a key pair (a public key and a private key) |
KeyPairGenerator | The KeyPairGenerator class is used to generate pairs of public and private keys. |
類
對於介面和類的描述,我直接引用了Oracle上的J2SE 7的API描述[3],就不翻譯成中文了,以防詞不達意。大家看英文應該能更精確的明白其意思。
利用上述的介面和類,就可以進行數字簽名和驗證了,下面分三部分進行基本步驟的描述。
第一部分:生成金鑰並存儲
- 生成KeyPairGenerator例項,並呼叫其genKeyPair()方法生成KeyPair物件。
- 利用ObjectOutputStream例項,將KeyPair物件寫到檔案中,從而把金鑰儲存到檔案中。
第二部分:進行數字簽名
- 從金鑰檔案中讀取KeyPair物件。
- 呼叫KeyPair物件的getPrivate()和getPublic()方法,分別獲取PrivateKey和PublicKey。
- 利用金鑰的指定演算法生成Signature例項,然後利用PrivateKey和檔案內容,分別呼叫其initSign()和update()方法,最後呼叫sign()方法生成數字簽名。
第三部分:進行簽名驗證
- 從金鑰檔案中讀取KeyPair物件。
- 呼叫KeyPair物件的getPrivate()和getPublic()方法,分別獲取PrivateKey和PublicKey。
- 利用金鑰的指定演算法生成Signature例項,然後利用PublicKey和檔案內容,分別呼叫其initSign()和update()方法,最後利用數字簽名呼叫verify()方法驗證簽名。
四、參考程式碼
根據上面的步驟描述,基本可以寫出程式來了。下面是參考程式碼,未必盡善盡美,但是基本功能都體現到了,供你參考。
工程結構:
- DataSecurity類:
package com.hzj.security;
import java.io.UnsupportedEncodingException;
import java.nio.charset.CharsetEncoder;
import java.security.KeyPair;
import com.hzj.util.StringHelper;
public class DataSecurity {
private KeyPair keyPair;
private static final String KEY_FILE = "/ca.key";
private DataSignaturer dataSignaturer;
public DataSecurity() {
try {
this.keyPair = KeyPairUtil.loadKeyPair(getClass().getResourceAsStream("/ca.key"));
this.dataSignaturer = new DataSignaturer(this.keyPair.getPublic(), this.keyPair.getPrivate());
} catch (RuntimeException e) {
System.out.println("沒有找到KeyPair檔案[/ca.key]!");
}
}
/**
* 驗證數字簽名
* @param data
* @param signs
* @return
*/
public boolean verifySign(String data, String signs) {
if ((data == null) || (signs == null)) {
System.out.println("引數為Null");
}
boolean verifyOk = false;
try {
verifyOk = this.dataSignaturer.verifySign(data.getBytes("UTF-8"), StringHelper.decryptBASE64(signs));
} catch (RuntimeException e) {
System.out.println("fail!data=" + data + ", sign=" + signs + ", exception:" + e.getMessage());
} catch (UnsupportedEncodingException e) {
System.out.println("不支援UTF-8字符集");
} catch (Exception e) {
System.out.println("Exception:" + e.getMessage());
}
if (!verifyOk) {
System.out.println("fail!data=" + data + ", sign=" + signs + ", verifyOk=false!");
}
return verifyOk;
}
/**
* 生成數字簽名
* @param data
* @return
*/
public String sign(String data)
{
if (data == null) {
System.out.println("引數為Null");
}
String sign = null;
try
{
sign = StringHelper.encryptBASE64(this.dataSignaturer.sign(data.getBytes("UTF-8")));
}
catch (UnsupportedEncodingException e)
{
System.out.println("不支援UTF-8字符集");
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
return sign;
}
}
DataSignaturer類:
package com.hzj.security;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
public class DataSignaturer {
private PrivateKey privateKey;
private PublicKey publicKey;
public DataSignaturer(PublicKey publicKey, PrivateKey privateKey){
this.privateKey = privateKey;
this.publicKey = publicKey;
}
/**
* 進行數字簽名
* @param data
* @return
*/
public byte[] sign(byte[] data) {
if (this.privateKey == null) {
System.out.println("privateKey is null");
return null;
}
Signature signer = null;
try {
signer = Signature.getInstance(this.privateKey.getAlgorithm());
} catch (NoSuchAlgorithmException e) {
System.out.println(e.getMessage());
}
try {
signer.initSign(this.privateKey);
} catch (InvalidKeyException e) {
System.out.println(e.getMessage());
}
try {
signer.update(data);
return signer.sign();
} catch (SignatureException e) {
System.out.println(e.getMessage());
return null;
} catch (NullPointerException e) {
System.out.println(e.getMessage());
return null;
}
}
/**
* 驗證數字簽名
* @param data
* @param signature
* @return
*/
public boolean verifySign(byte[] data, byte[] signature) {
if (this.publicKey == null) {
System.out.println("publicKey is null");
return false;
}
Signature signer = null;
try {
signer = Signature.getInstance(this.publicKey.getAlgorithm());
} catch (NoSuchAlgorithmException e) {
System.out.println(e.getMessage());
return false;
}
try {
signer.initVerify(this.publicKey);
} catch (InvalidKeyException e) {
System.out.println(e.getMessage());
return false;
}
try {
signer.update(data);
return signer.verify(signature);
} catch (SignatureException e) {
System.out.println(e.getMessage());
return false;
}
}
}
KeyPair類:
package com.hzj.security;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
public class KeyPairUtil {
// 採用的雙鑰加密演算法,既可以用DSA,也可以用RSA
public static final String KEY_ALGORITHM = "DSA";
/**
* 從輸入流中獲取KeyPair物件
* @param keyPairStream
* @return
*/
public static KeyPair loadKeyPair(InputStream keyPairStream) {
if (keyPairStream == null) {
System.out.println("指定的輸入流=null!因此無法讀取KeyPair!");
return null;
}
try {
ObjectInputStream ois = new ObjectInputStream(keyPairStream);
KeyPair keyPair = (KeyPair) ois.readObject();
ois.close();
return keyPair;
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
/**
* 將整個KeyPair以物件形式儲存在OutputStream流中, 當然也可以將PublicKey和PrivateKey作為兩個物件分別存到兩個OutputStream流中,
* 從而私鑰公鑰分開,看需求而定。
* @param keyPair 公鑰私鑰對物件
* @param out 輸出流
* @return
*/
public static boolean storeKeyPair(KeyPair keyPair, OutputStream out) {
if ((keyPair == null) || (out == null)) {
System.out.println("keyPair=" + keyPair + ", out=" + out);
return false;
}
try {
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(keyPair);
oos.close();
return true;
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
}
return false;
}
/**
* 生成KeyPair公鑰私鑰對
*
* @return
*/
public static KeyPair initKeyPair() throws NoSuchAlgorithmException{
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
return keyPairGen.genKeyPair();
}
/**
* 生成金鑰,並存儲
* @param out
* @return
* @throws NoSuchAlgorithmException
*/
public static boolean initAndStoreKeyPair(OutputStream out) throws NoSuchAlgorithmException {
return storeKeyPair(initKeyPair(), out);
}
}
StringHelper類:
package com.hzj.util;
import sun.misc.BASE64Encoder;
import sun.misc.BASE64Decoder;
public class StringHelper {
/**
* BASE64Encoder 加密
* @param data 要加密的資料
* @return 加密後的字串
*/
public static String encryptBASE64(byte[] data) {
BASE64Encoder encoder = new BASE64Encoder();
String encode = encoder.encode(data);
return encode;
}
/**
* BASE64Decoder 解密
* @param data 要解密的字串
* @return 解密後的byte[]
* @throws Exception
*/
public static byte[] decryptBASE64(String data) throws Exception {
BASE64Decoder decoder = new BASE64Decoder();
byte[] buffer = decoder.decodeBuffer(data);
return buffer;
}
}
Program類:
package com.hzj.main;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.security.NoSuchAlgorithmException;
import com.hzj.security.DataSecurity;
import com.hzj.security.KeyPairUtil;
public class Program {
public static void main(String[] args) {
// 1.生成證書
// File file = new File("ca.key");
// try {
// FileOutputStream fileOutputStream = new FileOutputStream(file);
// KeyPairUtil.initAndStoreKeyPair(fileOutputStream);
// } catch (FileNotFoundException | NoSuchAlgorithmException e) {
// e.printStackTrace();
// }
// 2.生成數字簽名
// DataSecurity dataSecurity = new DataSecurity();
// String sign = dataSecurity.sign("大家好");
// System.out.println("sign:" + sign);
//3.驗證數字簽名
DataSecurity dataSecurity = new DataSecurity();
boolean result = dataSecurity.verifySign("大家好", "MCwCFCDs3sBw/fXK9flndl0M5lAUiPYFAhR9vyNNc91UiUBxFwK3GzLLjWgTkQ==");
System.out.println("result:" + result);
}
}
這裡需要注意的是,為什麼要對數字簽名進行進行Base64編碼呢?這是因為生成的數字簽名是byte[]型的,無論對應哪一種字符集來轉化成String,都會有亂碼出現。所以,採用Base64進行編碼,就可以得到一串可見的字串,方便儲存和重新呼叫。
五、後記
寫到這裡,本文的內容就基本上完結了。有人看到這裡就會問,這不是說資料庫記錄防篡改嘛,一直都在講數字簽名,究竟怎麼個防篡改?前文已經對數字簽名的一些基本原理,使用的場景,開發的步驟、程式碼等進行了描述,開篇是也描述了專案中遇到的資料庫中的問題。將這些資訊綜合起來,應該就知道怎麼將數字簽名應用到資料庫記錄中來作為資料庫防篡改的工具了。知道工具怎麼用是基礎,會用工具來完成自己想做的事情,就是進階了。祝你步步高昇!
六、參考資料:
[1] http://www.ruanyifeng.com/blog/2006/12/notes_on_cryptography.html
[2] http://www.youdzone.com/signature.html
[3] http://docs.oracle.com/javase/7/docs/api/java/security/package-summary.html