使用数字签名实现数据库记录防篡改(Java实现)
本文大纲
一、提出问题
二、数字签名
三、实现步骤
四、参考代码
五、后记
六、参考资料
一、提出问题
最近在做一个项目,需要对一个现成的产品的数据库进行操作,增加额外的功能。为此,需要对该产品对数据库有什么操作进行研究(至于怎么监控一个产品的操作会引发什么数据库操作,以后会详细解说)。本来已经对数据库的操作了如指掌的,无意中发现数据库表里的每条记录都会有这样一个字段:
这感觉不妙了,字段名叫signature,顾名思义,就是签名的意思呀。难道数据库表中的每条记录都会有签名?也就是说如果我不能正确生成签名,而直接改记录中的字段,会被程序认为非法篡改了数据?那以后我的产品设计,是否也可采用这种方式来对每条记录做签名,防止数据被非法篡改,例如日志表中的数据?抱着这一发现以及这一连串的问题,我进行了以下的研究。在这里我将研究整理了一下,分享给大家。
二、数字签名
要解决上面的问题,首先就要对最基础的知识进行了解。这里最基础的知识,无疑就是什么是数字签名了。很多同学可能对这个名词并不陌生,但估计大多数人都是对其一知半解,会把散列、非对称加密、数字签名、数字证书的几个概念混为一谈,造成混乱。所以我先对相关概念进行解释,再往下讲。如果很熟悉这方面的同学可以跳过此部分,但对于绝大多数同学来说,不建议这样做。基础没搭好,直接看怎么实现,换了个说法又不知道怎么去做了。要想提高个人能力,做到举一反三很重要。
言归正传,先对跟数字签名有关的密码学知识简单说一下。加密方法分两大类,分别是单钥加密和双钥加密,数字签名涉及到双钥加密。关于双钥加密,主要涉及到以下几个要点[1]:
- 双钥加密的密钥有两把,一把是公开的公钥,一把是不公开的私钥
- 公钥和私钥是一一对应的关系,有一把公钥就必然有一把与之对应的、独一无二的私钥,反之亦成立。
- 所有的(公钥, 私钥)对都是不同的。
- 用公钥可以解开私钥加密的信息,反之亦成立。
- 同时生成公钥和私钥应该相对比较容易,但是从公钥推算出私钥,应该是很困难或者是不可能的。
- 在双钥体系中,公钥用来加密信息,私钥用来数字签名。
- 还有一点关于数字证书的。因为任何人都可以生成自己的公钥私钥对,所以为了防止有人散布伪造的骗取信任,就需要一个可靠的第三方机构来生成经过认证的公钥、私钥对。简单来说,数字证书是权威的第三方机构颁发的,用来认证某对公钥私钥的证书,经过这个数字证书认证的公钥私钥,就可以明确属于某人或者某机构,是合法的,可信任的。就如同身份证,是证明你身份的一个证件。所以数字证书跟数字签名是两回事,要分清楚。
数字签名,顾名思义,就类似于一种写在纸上的普通的物理签名,不同的是,数字签名是电子信息化的,采用双钥加密的技术实现,是一种用于鉴别数字信息的方法。处理的过程,简单说就是将文件内容进行hash散列,信息发送者对散列后的字符串使用私钥加密,得到的最终字符串就是签名。然后将得到的签名字符串添加到文件信息的后面一同发送出去。接收者获取到文件信息和签名后,使用公钥对签名进行解密,就得到文件内容加密后的hash散列。此时,他可以对获取到的文件内容做hash散列,与签名中的hash散列进行匹对,从而鉴别出最终获取信息的真伪。主要过程如这四幅图所示[2]:
对文件内容进行hash散列,生成摘要
对生成的摘要,使用私钥进行加密,形成签名
将得到的签名,附到文件内容后部,就想到与签名签到文件尾部那样子
使用公钥对签名进行解密,得到摘要,并与获取到的文件内容生成的摘要做对比,以确定是否被篡改
想了解更详细的数字证书相关内容,可以访问此地址:http://www.youdzone.com/signature.html。里面解释得很形象,应该一看就明白的了。
三、实现步骤
看到这里,开篇提出的问题也就呼之欲出了。没错,就是使用数字签名技术,将数据库中的重要字段进行签名,将签名结果作为记录的一列存在记录中。这样当有人入侵数据库,恶意修改字段,程序读数据时拿签名校验一下,就知道数据是否有被修改过了。
在java.security包中,有很多有用的类,用以进行安全机制的开发。对于要创建数字签名,我们主要用到以下的接口或类:
接口名 | 描述 |
接口
类名 | 描述 |
类
对于接口和类的描述,我直接引用了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