Showing posts with label KeyGenerator. Show all posts
Showing posts with label KeyGenerator. Show all posts

Thursday, January 16, 2014

Java Cryptography - KeySpec & KeyFactory

上篇文章中我们说道:在进行加密/解密的过程中,我们需要使用Key。但是Key接口没有提供用于获取Key中属性的方法。如果想要获取Key中的属性,需要通过KeySpec。

Key和KeySpec之间的转换需要借助于KeyFactory来完成

KeySpec

KeySpec是一个接口,同时也代表了一系列类——Key Specification。前面说过,Key接口是Key的不透明表现形式(Opaque Representation);与之相对,KeySpec是Key的透明表现形式(Transparent Representation),可以从Key Specification中得到一些Key内部的属性。

对于每种加密算法,有各自的Key Specification实现,里面包含了对应算法的Key所需要的信息。比如RSA Public Key的Key Specification(RSAPublicKeySpec),包含了modulus和publicExponent属性;而DSA Public Key的Key Specification(DSAPublicKeySpec),包含了DSA算法中需要的p, q, g, y属性。

同时我们也可以看到在KeySpec接口中,没有包含任何方法或者属性。这个接口仅仅用于标识对象的类型,并不提供实际的功能。

注意:一种加密算法的Key,可能会对应多种KeySpec。比如RSA Public Key就对应了两种KeySpec:RSAPublicKeySpec、X509EncodedKeySpec。那么,这两种KeySpec有什么不同呢?

KeySpec的分类

按照KeySpec对数据的组织形式不同,我们可以将KeySpec大致分为两类:

  • 参数形式:将key中的数据组织成不同的参数,保存在属性中。比如我们之前所说,我想要获取RSA Public Key的modulus信息,就可以使用RSAPublicKeySpec,通过它的getModulus方法来获得modulus信息。当然,也可以通过setModulus方法来通来设置KeySpec的modulus属性。
  • 编码形式:将key中的数据编码后(以下称之为Encoded Key)保存在属性中。有点类似于Key接口中的getEncoded方法。但是,还记得吗?通过Key接口,只能获取Encoded Key,但是,没有办法利用Encoded Key重新构造Key。利用编码形式的KeySpec就可以实现这一点。比如X509EncodedKeySpec,我们可以在构造方法中传入X509编码Encoded Key,由此来构造一个X509EncodedKeySpec。当然,也可以使用getEncoded方法,获取X509编码的Key。

SecretKeySepc

对称加密算法的Key相对比较简单,它们使用的都是SecretKey,其内部存储的都是一个原始的byte数组。因此,尽管算法不同,但是Key Specification中的数据的表现形式都是一样的。因此Java为对称加密算法提供了通用的Key Specification——SecretKeySpec。

我们可以通过传入原始的Encoded Key以及算法名称来构造一个SecretKeySpec。如果想要为其它的算法生成SecretKeySpec,只需要将调用构造方法时传入的算法名称改成需要的即可。但是这里有一点需要注意一下:输入的Encoded Key必须要符合特定算法要求的Key Size,否则当Key在使用的时候会抛出异常。

当然,针对某些对称加密算法,Java也提供了更加具有针对性的Key Specification。比如DES算法。我们可以使用通用的SecretKeySpec类来构建DES Key。同时,Java也提供了DESKeySpec类,来单独的完成DES Key Specification的相关操作。通过源码可以看到,DESKeySpec类中加入了一些对于Key的验证的逻辑。

KeyFactory & SecretKeyFactory

除了SecretKeySpec能够直接被当作Key使用以外,大多数的KeySpec都不能被当作Key直接被用于加密解密。为了在Key和KeySpec之间转换,我们需要利用KeyFactory和SecretKeyFactory.

KeyFactory & SecretKeyFactory的作用

  1. 利用Key构造对应的KeySpec
  2. 利用KeySpec构造对应的Key

KeyFactory和SecretKeyFactory有什么区别呢?没错,你猜得对。KeyFactory是作用于非对称密钥(Asymmetric Key)的,SecretKeyFactory是作用于对称密钥(Symmetric Key)的。我一直觉得在JCA中的Generator和Factory的命名比较奇怪:作用于对称密钥的是KeyGenerator & SecretKeyFactory,作用于非对称密钥的是KeyPairGenerator & KeyFactory。注意到了么?Generator和Factory的前缀正好是对调的。不知道Sun当时为什么要这样命名,是否是有什么历史原因,我没有深究。

因为KeyFactory和SecretKeyFactory在使用上基本是一样的,通过它们暴露的接口也可以发现,基本所有的方法签名都是一致的。因此,下面虽然用的是KeyFactory,但是对于SecretKeyFactory同样适用。只需要记住,在对称加密算法的时候使用SecretKeyFactory;在非对称加密算法中使用KeyFactory即可。

KeyFactory使用方法

KeyFactory是Engine类,每当我意识到某个类是Engine类的时候,我总是很高兴,因为我基本不需要看文档就知道大概的用法了。

  1. 调用getInstace方法,从JCA中请求KeyFactory实例
  2. 调用KeyFactory中提供的方法,从而实现Key和KeySpec之间的转换。
Key -> KeySpec

把Key转换为KeySpec需要解决两个问题,也就是说KeyFactory需要解决这两个问题:

  1. 每种算法的Key和KeySpec的构成都是有区别的,所以不同算法的Key需要使用不同的KeyFactorySpi实现;

    可以通过在getInstace的时候,传入对应的算法名称解决

  2. 一个Key可能会对应多种KeySpec(还记得KeySpec的数据组织形式么?),因此我们在通过KeyFactory获取KeySpec的时候,需要让KeyFactory知道我们需要的是哪一种KeySpec。

    在获取KeySpec的时候,通过传入KeySpec的类型来告知KeyFactory我们需要哪种类型的KeySpec

下面是使用SecretKeyFactory将SecretKey转换成KeySpec的例子:

public static DESKeySpec getDESKeySpec(SecretKey key) {
    try {
        //获取实现了DES算法的SecretKeyFactory
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
        //获取DESKeySpec
        DESKeySpec keySpec = (DESKeySpec)keyFactory.getKeySpec(key, DESKeySpec.class);
        return keySpec;
    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
        throw new RuntimeException(ex);
    }
}
   

我们再来看一个KeyFacotry的例子:

public static KeySpec getRSAPrivateKeySpec(Key key) {
    try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        //获取参数形式的KeySpec
        RSAPrivateKeySpec parameterKeySpec = keyFactory.getKeySpec(key, RSAPrivateKeySpec.class);

        //获取编码形式的KeySpec
        PKCS8EncodedKeySpec encodedKeySpec = keyFactory.getKeySpec(key, PKCS8EncodedKeySpec.class);

        //根据需要返回特定的KeySpec
        return encodedKeySpec;
        //return parameterKeySpec;
    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
        throw new RuntimeException(ex);
    }
}
   
KeySpec -> Key

同样,这个转换也是需要由KeyFactory来完成。因为一个KeySpec只能对应于一种Key,所以将KeySpec转换成Key的时候,不再需要传入目标Key的类型了。只需要调用相应的方法,传入KeySpec,就可以得到对应的Key。

  • SecretKeyFactory中调用generateSecret方法。
  • KeyFactory中调用generatePublic(公钥)或者generatePrivate(私钥)。

下面的代码将DESKeySpec转换成SecretKey:

public static SecretKey generateDESSecretKey(DESKeySpec keySpec) {
    try {
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
        SecretKey key = keyFactory.generateSecret(keySpec);
        return key;
    } catch (NoSuchAlgorithmException ex) {
        throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
        throw new RuntimeException(ex);
    }
}
   

同样,在给出一个转换RSAKeyPrivateKeySpec到RSAPrivateKey的例子:

public static RSAPrivateKeySpec getRSAPrivateKeySpec(Key key) {
    try {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        RSAPrivateKeySpec keySpec = keyFactory.getKeySpec(key, RSAPrivateKeySpec.class);
        return keySpec;
    } catch (NoSuchAlgorithmException ex) {
        throw new RuntimeException(ex);
    } catch (InvalidKeySpecException ex) {
        throw new RuntimeException(ex);
    }
}
   

SecretKeySpec & SecretKey的特殊性

SecretKeySpec和SecretKey是Java默认实现的对称加密算法的通用Key。注意,他们之间不能通过SecretKeyFactory进行转换。其实想想,也无需使用SecretKeyFactory,它们的转换方法如下:

  1. 如果要把SecretKey转换成SecretKeySpec,可以直接通过SecretKey.getEncoded方法获取encoded key,然后在构造SecretKeySpec的时候,直接通过构造方法传入encoded key即可;
  2. 如果要把SecretKeySpec转换成SecretKey则更加简单,完全不需要额外的操作,因为SecretKeySpec已经继承了SecretKey,也就是说SecretKeySpec就是SecretKey,可以直接使用。

最后,给出一个Key与KeySpec的对应关系表

Symmetric Key

Algorithm Key KeySpce
DES DESKey DESKeySpec
Symmetric SecretKey SecretKeySpec

Asymmetric Key

Algorithm Key KeySpce
DiffieHellman DHPublicKey DHPublicKeySpec
X509EncodedKeySpec
DHPrivateKey DHPrivateKeySpec
PKCS8EncodedKeySpec
DSA DSAPublicKey DSAPublicKeySpec
X509EncodedKeySpec
DSAPrivateKey DSAPrivateKeySpec
PKCS8EncodedKeySpec
RSA RSAPublicKey RSAPublicKeySpec
X509EncodedKeySpec
RSAPrivateKey RSAPrivateKeySpec
RSAPrivateCrtKeySpec
PKCS8EncodedKeySpec

总结

Key在加密/解密过程中是不可或缺的,没有Key一切都无从谈起。

Key的生成需要使用Generator类:KeyGenerator 和 KeyPairGenerator

想要获取Key中的属性,需要将Key转换为KeySpec,然后,从KeySpec中获得需要的属性

想要利用已有的Key的属性重建Key,也需要先去构造KeySpec,并且设置好Key的属性,然后,才能将KeySpec转换为Key

SecretKey是所有对称加密算法通用的Key类型。同样,SecretKeySpec也是所有对称加密算法通用的KeySpec类型

Key和KeySpec之间的转换需要通过Factory来完成(SecretKeyFactory, KeyFactory)

SecretKey和SecretKeySpec之间的转换不能使用KeyFactory

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。