Wednesday, January 8, 2014

Java Cryptography - Key

无论是哪一种加密算法,Key(密钥)都是不可或缺的。根据加密算法的不同,相应的key也是不一样的。在进行加密/解密的过程中,我们可能会涉及到这几个问题:Key的生成;Key的保存;根据保存的数据重新构造Key。下面将会对这几个问题一一讲解。

Key的分类

首先根据加密算法种类的不同,key可以分成两大类:
  1. SecretKey-对称密钥:对应于对称加密算法(Symmetric algorithm),即加密密钥和解密密钥使用的是同一个密钥,代表算法有DES, AES 等。
  2. KeyPair-非对称密钥;对应于非对称加密算法(Asymmetric algorithm),即分为Public Key(公钥)和Private Key(私钥)。使用Public Key加密的信息,只有用Private Key才能够解密;反之,使用Private Key加密的信息,也只有用对应的Public Key才能解密。代表算法有DSA, RSA 等。

Key的生成

根据Key的种类不同,生成方式也不一样。我们主要使用两个类来进行Key的生成:
  • KeyGenerator类,用于生成SecretKey
  • KeyPairGenerator类,用于生成KeyPair

KeyGenerator

使用KeyGenerator来生成SecretKey非常简单。首先,这是一个Engine类。好了那么我们就知道了它大概的用法:
  1. 调用静态的getInstance方法,通过Algorithm和Provider获取一个KeyGenerator的实例
  2. 调用init方法对KeyGenerator的实例进行初始化:主要是指定一个Key Size和一个可选的随机数生成器(RNG),如果没有传入RNG,那么KeyGenerator将会使用默认的RNG——SecureRandom
  3. 调用generateKey方法来生成Secretkey
下面是生成DES Key的示例代码:
public static SecretKey generateDESKey() {
    try {
            KeyGenerator kg = KeyGenerator.getInstance("DES");
            /**
             * 注意在SunJCE的默认实现中,要求DES Key Size必须是56,
             * 此处如果传入其它数值,将会抛出InvalidParameterException("Wrong keysize: must be equal to 56")
             * 详情请参见com.sun.crypto.provider.DESKeyGenerator.engineInit() 方法
             */
            kg.init(56);
            SecretKey key = kg.generateKey();
            return key;
    } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex);
    }
}
    
每种加密算法对Key都有不同的要求,尤其是Key Size,根据算法的不同,需要不同的Key Size。下表来自于Java官方文档,里面列出了JDK默认实现的加密算法所需要的Key Size,以及默认值:
Algorithm Name Default Keysize Restrictions/Comments
AES 128 Keysize must be equal to 128, 192, or 256.
ARCFOUR (RC4) 128 Keysize must range between 40 and 1024 (inclusive).
Blowfish 128 Keysize must be a multiple of 8, ranging from 32 to 448 (inclusive).
DES 56 Keysize must be equal to 56.
DESede (Triple DES) 168 Keysize must be equal to 112 or 168.
A keysize of 112 will generate a Triple DES key with 2 intermediate keys, and a keysize of 168 will generate a Triple DES key with 3 intermediate keys.
Due to the "Meet-In-The-Middle" problem, even though 112 or 168 bits of key material are used, the effective keysize is 80 or 112 bits respectively.
HmacMD5 512 No keysize restriction.
HmacSHA1 512 No keysize restriction.
HmacSHA256 256 No keysize restriction.
HmacSHA384 384 No keysize restriction.
HmacSHA512 512 No keysize restriction.
RC2 128 Keysize must range between 40 and 1024 (inclusive).

KeyPairGenerator

在说KeyPairGenerator之前,有必要先说一下KeyPair类。
KeyPair类没有什么实际的功能,其实它的唯一作用就是创建了一个类,用于组织Public Key和Private Key。对应的key保存在这个类中对应的属性里面(publicKey & privateKey)。我们可以通过调用getPublic和getPrivate方法获取这相应的Key。
KeyPairGenerator的使用方法和KeyGenerator基本一样,因为它也是一个Engine类:
  1. 调用静态的getInstance方法,通过Algorithm和Provider获取一个KeyPairGenerator的实例
  2. 调用initialize方法对KeyPairGenerator的实例进行初始化:主要是指定一个Key Size和一个可选的随机数生成器(RNG),如果没有传入RNG,那么KeyPairGenerator将会使用默认的RNG——SecureRandom
  3. 调用genKeyPair方法或者generateKeyPair方法来生成KeyPair。(genKeyPair方法内部直接调用了generateKeyPair方法)
下面是使用KeyPairGenerator生成RSA KeyPair的示例代码:
public static KeyPair generateRSAKeyPair() {
    try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048);
            KeyPair keyPair = kpg.genKeyPair();
            return keyPair;
    } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex);
    }
}
        
对于KeyPairGenerator来说,每种算法对于Key Size也有不同的要求。而且,即使是相同的算法,在不同的Provider实现中要求的Key Size也是不一样的。具体信息可以查看Java的官方文档(http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html)。
比如上面所用的RSA算法,在Java的安装文件中有两个Provider都提供了RSA算法的实现:SunRSASign Provider和SunMSCAPI Provider。在SunRSASign中要求Key Size在[512 bits, 65536 bits]之间;而在SumMSCAPI中要求RSA的Key Size在 [512 bits, 16384 bits]之间。所以,如果不清楚算法实现对Key Size的要求,最好的方法就是查看上面提到的Java官方文档,或者通过查看源码,自己发现要求的KeySize。

Key接口

在Java中,Key接口是所有具体Key的接口。无论是Symmetric Key还是Asymmetric Key都需要实现Key接口。Key接口是Key的一种不透明的表现形式(Opaque Representation),与之相对的是透明的表现形式KeySpec(Transparent Representation)。关于KeySpec的相关信息这里暂时不说。Key接口中只提供了非常有限的三个方法访问Key中封装的数据:
/**
 * 返回能够使用这个Key的加密算法。
 * 比如生成的是AES Key,那么这个方法将返回"AES"。
 */
public String getAlgorithm();

/**
 * 返回Key的编码格式。
 * Key的种类不同,保存的信息也不一样。
 * 有的Key比较简单,只是保存了一些字节比如AES和DES,
 * 调用这些Key的getFormat方法将返回RAW,意思是原本的字节信息;
 * 另外有些key包含的信息就比较复杂了,
 * 比如RSA的Public Key,里面包含了modulus以及publicExponent,
 * 因此就需要把这些信息按照一定的编码方式来进行编码。
 * 通过调用getFormat这个方法,就会返回使用的编码方式。
 */
public String getFormat();

/**
 * 这个方法根据getFormat方法返回的编码方式,对Key进行编码,并返回编码后的结果。
 * 这个方法可以用于将程序中生成的Key导出为byte 数组的形式。
 * 利用导出的byte数组,我们可以完成许多功能。
 * 比如将byte数组保存在文件中,这样就将Key持久化到了介质上;
 * 或者将byte数组通过网络传输给另一个太机器,实现Key的共享等等。
 */
public byte[] getEncoded();
    
介绍了Key接口中的方法,我们就可以明白为什么说“Key接口是Key的不透明表现形式(Opaque Representation)”了。因为通过Key接口我们不能得到Key中的各项信息,能够得到的仅仅是经过编码后的Key。比如我无法从Key中得到RSA Public Key里面的modulus信息。
下面是将key导出到文件的一个简单例子:
public static void saveKey(Key key, Path path) {
    //获取编码后的key
    byte[] keyData = key.getEncoded();

    //将编码后的key转换成base64的字符串,方便在文件中查看
    String keyStr = DatatypeConverter.printBase64Binary(keyData);

    try {
            Files.write(path, keyStr.getBytes("UTF-8"), StandardOpenOption.WRITE);
    } catch (IOException ex) {
            throw new RuntimeException(ex);
    }
}
 

Key接口的局限

我们可以看到在Key接口中只提供了getEncoded方法,并没有对应的set方法。那么这样就有个问题了,我将Key导出成byte 数组之后,如何通过这个byte 数组还原Key呢?
另外,如果我就是想要获取Key中的信息呢?比如我想看看我的RSA Public Key中modulus到底是什么,应该怎么办呢?
想要回答以上两个问题,仅仅使用Key接口是做不到的。需要用到KeySpec和KeyFactory。

1 comment :