Java Cipher初探

2,157 阅读13分钟

在编写项目的时候由于要使用SSL,因此我使用到了Cipher这个类,这个类在jdk文档的描述为:

This class provides the functionality of a cryptographic cipher for encryption and decryption. It forms the core of the Java Cryptographic Extension (JCE) framework.

此类提供用于加密和解密的加密密码的功能。 它构成了Java Cryptographic Extension(JCE)框架的核心。

描述和用法相同,通过从密钥库或证书的加密类型来获取对应的加密解密的功能。

因为好奇,因此我决定看看它的源码来了解RSA具体的加密过程。

首先是该类的使用加密Demo:


    /**
     * 最大加密大小
     */
    private static final MAX_ENCRYPT_BLOCK = 117;

    public byte[] encryptByPrivateKey(byte[] data) throws Exception {
        //根据密钥库相关信息获取私钥对象
        PrivateKey privateKey = getPrivateKey(keyStorePath, alias, password);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        int inputLen = data.length;
        int offSet = 0;
        int i = 0;
        byte[] cache;
        while (inputLen - offSet > 0) {
            if (intputLen - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipler.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
            } else {
                cache = cipler.doFinal(data, offSet, inputLen - offSet);
            }
            out.write(cache, 0, cache.length);
            i++;
            offSet = i * MAX_ENCRYPT_BLOCK;
        }
        return out.toByteArray();
    }

从上述代码可以知道在这个Demo中Cipher的调用顺序为:

  1. 通过标准密钥名称初始化加密解密功能。
  2. 初始化密钥。设定加密还是解密的状态以及对应的密钥。
  3. 按字节数组进行加密并返回加密结果。

因此这篇文章就会通过这个顺序来探讨Cipher对数据加密的流程以及实现。

初始化加密解密功能

首先调用的是getInstance(String transformation)方法,该方法在文档中的描述为:

Returns a Cipher object that implements the specified transformation.

返回实现指定转换的Cipher对象。

和程序使用的目的一样,然后进入该方法内部查看源码:

    public static final Cipher getInstance(String var0) throws NoSuchAlgorithmException, NoSuchPaddingException {
        List var1 = getTransforms(var0);
        ArrayList var2 = new ArrayList(var1.size());
        Iterator var3 = var1.iterator();

        while(var3.hasNext()) {
            Cipher.Transform var4 = (Cipher.Transform)var3.next();
            var2.add(new ServiceId("Cipher", var4.transform));
        }

        List var11 = GetInstance.getServices(var2);
        Iterator var12 = var11.iterator();
        Exception var5 = null;

        while(true) {
            Service var6;
            Cipher.Transform var7;
            int var8;
            do {
                do {
                    do {
                        if (!var12.hasNext()) {
                            throw new NoSuchAlgorithmException("Cannot find any provider supporting " + var0, var5);
                        }

                        var6 = (Service)var12.next();
                    } while(!JceSecurity.canUseProvider(var6.getProvider()));

                    var7 = getTransform(var6, var1);
                } while(var7 == null);

                var8 = var7.supportsModePadding(var6);
            } while(var8 == 0);

            if (var8 == 2) {
                return new Cipher((CipherSpi)null, var6, var12, var0, var1);
            }

            try {
                CipherSpi var9 = (CipherSpi)var6.newInstance((Object)null);
                var7.setModePadding(var9);
                return new Cipher(var9, var6, var12, var0, var1);
            } catch (Exception var10) {
                var5 = var10;
            }
        }
    }

方法很大,这里我根据代码格局看。

首先是

    List var1 = getTransforms(var0);
    ArrayList var2 = new ArrayList(var1.size());
    Iterator var3 = var1.iterator();

这三行实际是对传入的加密方式的操作,其中的var0就是我们传入的加密类型。那么有难度的就第一行的getTransforms(var0)方法,这个方法是可以从它的返回值进行猜测,参数是String,返回值是List,通常这种方法的作用都是对字符串进行分割。在此之前先确定我们传入的参数:var0 = "RSA"。

由于该方法和文章主题无关,因此我这里就直接一步骤带过:该方法返回一个具体类型为SingletonList的存储着加密类型及相关信息对象的不可变的列表。

在该样例中只有一个存储着相关加密信息的类。

然后是

    while(var3.hasNext()) {
        Cipher.Transform var4 = (Cipher.Transform)var3.next();
        var2.add(new ServiceId("Cipher", var4.transform));
    }

通过遍历var3获取对应的加密类Cipher.Transform并将它的加密信息保存在ServiceId中放入var2中。而该信息为:"RSA"。

接着是

    List var11 = GetInstance.getServices(var2);
    Iterator var12 = var11.iterator();
    Exception var5 = null;

又是传入参数并返回List,这个方法的作用以我的理解为:传入密钥类型获取对应的加密解密服务列表,其中保存的元素类型为Service,在源码中对该类的描述为:

The description of a security service. It encapsulates the properties of a service and contains a factory method to obtain new implementation instances of this service.

安全服务的描述。 它封装了服务的属性,并包含一个工厂方法来获取此服务的新实现实例。

可知var11包含着加密解密服务的属性。

然后是一个while(true)循环,由于代码比较多因此也是按功能进行阅读:

首先是:

    Service var6;
    Cipher.Transform var7;
    int var8;

这是一个初始化过程,没什么好说的。

然后是

    do {
        do {
            do {
                if (!var12.hasNext()) {
                    throw new NoSuchAlgorithmException("Cannot find any provider supporting " + var0, var5);
                }

                var6 = (Service)var12.next();
            } while(!JceSecurity.canUseProvider(var6.getProvider()));

            var7 = getTransform(var6, var1);
        } while(var7 == null);

        var8 = var7.supportsModePadding(var6);
    } while(var8 == 0);

这是一个对服务以及加密方式的遍历过程,这一步就和该方法的描述一样:

This method traverses the list of registered security Providers, starting with the most preferred Provider. A new Cipher object encapsulating the CipherSpi implementation from the first Provider that supports the specified algorithm is returned.

此方法遍历已注册的安全提供程序列表,从最首选的提供程序开始。 将返回一个新的Cipher对象,该对象封装来自第一个支持指定算法的Provider的CipherSpi实现。

在提供的服务列表中遍历并获取到参数对应的服务然后获取它的支持模式填充,具体的就是supportsModePadding方法。该方法会获取提供的Cipher.Transform对象的属性来得到对应的模式。在该例子中返回的模式代码为2。即:var8 = 2。

然后

    if (var8 == 2) {
        return new Cipher((CipherSpi)null, var6, var12, var0, var1);
    }

当状态为2,由上可知我们满足这个判断语句因此返回一个新的Cipher对象。其中的参数描述从上面的阅读中我们可以知道分别是:未知、服务对象、服务列表、密钥名称、保存着密钥名称信息的Cipher.Transform类。

初始化密钥

有上面可知,getInstance方法返回的Cipher对象中只有密钥和密钥相关的服务。并不知道是加密还是解密,因此初始化就十分重要。

从该方法点进去可以看到

    public final void init(int var1, Key var2) throws InvalidKeyException {
        this.init(var1, var2, JceSecurity.RANDOM);
    }

最终具体的方法体为


    public final void init(int var1, Key var2, SecureRandom var3) throws InvalidKeyException {
        this.initialized = false;
        checkOpmode(var1);
        if (this.spi != null) {
            this.checkCryptoPerm(this.spi, var2);
            this.spi.engineInit(var1, var2, var3);
        } else {
            try {
                this.chooseProvider(1, var1, var2, (AlgorithmParameterSpec)null, (AlgorithmParameters)null, var3);
            } catch (InvalidAlgorithmParameterException var5) {
                throw new InvalidKeyException(var5);
            }
        }

        this.initialized = true;
        this.opmode = var1;
        if (!skipDebug && pdebug != null) {
            pdebug.println("Cipher." + this.transformation + " " + getOpmodeString(var1) + " algorithm from: " + this.provider.getName());
        }

    }

我们先从最开始看起

    this.init(var1, var2, JceSecurity.RANDOM);

这里有新增加了一个JceSecurity.RANDOM参数,这个参数的具体类型为SecureRandom,从源码中查看该类的描述:

Constructs a secure random number generator (RNG) implementing the default random number algorithm.

构造一个实现默认随机数算法的安全随机数生成器(RNG)。

原来是一个随机数生成器。那么继续看下面的代码

    this.initialized = false;
    checkOpmode(var1);

这两个分别是初始化条件设定,并判断传入的模式是否正确。

然后是

    if (this.spi != null) {
        this.checkCryptoPerm(this.spi, var2);
        this.spi.engineInit(var1, var2, var3);
    } else {
        try {
            this.chooseProvider(1, var1, var2, (AlgorithmParameterSpec)null, (AlgorithmParameters)null, var3);
        } catch (InvalidAlgorithmParameterException var5) {
            throw new InvalidKeyException(var5);
        }
    }

判断传入的CipherSpi是否存在。这里我就直接按照这个Demo中的条件来阅读,即CipherSpi不存在。那么执行的代码就是

    try {
        this.chooseProvider(1, var1, var2, (AlgorithmParameterSpec)null, (AlgorithmParameters)null, var3);
    } catch (InvalidAlgorithmParameterException var5) {
        throw new InvalidKeyException(var5);
    }

其中的参数为:1,加密模式:1,私钥对象,null,null,安全的随机数生成器。 由于chooseProvider方法代码比较多因此先看后续步骤

    this.initialized = true;
    this.opmode = var1;

这里就是对相关属性的赋值,其中initialized表示初始化完成,opmode表示打开的模式为加密模式(1)。

然后我们查看chooseProvider方法的方法体

    private void chooseProvider(int var1, int var2, Key var3, AlgorithmParameterSpec var4, AlgorithmParameters var5, SecureRandom var6) throws InvalidKeyException, InvalidAlgorithmParameterException {
        Object var7 = this.lock;
        synchronized(this.lock) {
            if (this.spi != null) {
                this.implInit(this.spi, var1, var2, var3, var4, var5, var6);
            } else {
                Exception var8 = null;

                while(true) {
                    Service var9;
                    CipherSpi var10;
                    Cipher.Transform var11;
                    do {
                        do {
                            do {
                                do {
                                    if (this.firstService == null && !this.serviceIterator.hasNext()) {
                                        if (var8 instanceof InvalidKeyException) {
                                            throw (InvalidKeyException)var8;
                                        }

                                        if (var8 instanceof InvalidAlgorithmParameterException) {
                                            throw (InvalidAlgorithmParameterException)var8;
                                        }

                                        if (var8 instanceof RuntimeException) {
                                            throw (RuntimeException)var8;
                                        }

                                        String var16 = var3 != null ? var3.getClass().getName() : "(null)";
                                        throw new InvalidKeyException("No installed provider supports this key: " + var16, var8);
                                    }

                                    if (this.firstService != null) {
                                        var9 = this.firstService;
                                        var10 = this.firstSpi;
                                        this.firstService = null;
                                        this.firstSpi = null;
                                    } else {
                                        var9 = (Service)this.serviceIterator.next();
                                        var10 = null;
                                    }
                                } while(!var9.supportsParameter(var3));
                            } while(!JceSecurity.canUseProvider(var9.getProvider()));

                            var11 = getTransform(var9, this.transforms);
                        } while(var11 == null);
                    } while(var11.supportsModePadding(var9) == 0);

                    try {
                        if (var10 == null) {
                            var10 = (CipherSpi)var9.newInstance((Object)null);
                        }

                        var11.setModePadding(var10);
                        this.initCryptoPermission();
                        this.implInit(var10, var1, var2, var3, var4, var5, var6);
                        this.provider = var9.getProvider();
                        this.spi = var10;
                        this.firstService = null;
                        this.serviceIterator = null;
                        this.transforms = null;
                        return;
                    } catch (Exception var14) {
                        if (var8 == null) {
                            var8 = var14;
                        }
                    }
                }
            }
        }
    }

代码还是很多,因此我按照功能来阅读

首先是

Object var7 = this.lock;

从参数名称就可以知道这是作为并发执行的时候的安全锁。但是在这个Demo中该参数为null。

然后是

synchronized(this.lock) {
    if (this.spi != null) {
        this.implInit(this.spi, var1, var2, var3, var4, var5, var6);
    } else {
        ...
    }
}

这里表示其中的操作为原子操作,不允许多个线程执行,按照Demo的条件判断程序进入的是else的代码。

接着是

Exception var8 = null;

这里定义了一个线程池。外部原子操作,内部有线程池,一个初始化操作为什么要涉及到多线程呢?继续往下

while(true) {
    Service var9;
    CipherSpi var10;
    Cipher.Transform var11;
    
    ...
}

这里又是定义参数。其中有服务Service,CipherSpi和密钥信息Cipher.Transform。按照Demo的条件这里我猜测三个参数的值分别是:RSA的服务,null和RSA的信息。

接下来的代码为多个do-while语句嵌套,这里我从里向外阅读,首先是

if (this.firstService == null && !this.serviceIterator.hasNext()) {
    ...
}

这个条件的判断需要不存在对应的服务才能满足,在这个Demo中显然还是存在服务的,所以直接跳过。

然后是

if (this.firstService != null) {
    var9 = this.firstService;
    var10 = this.firstSpi;
    this.firstService = null;
    this.firstSpi = null;
} else {
    var9 = (Service)this.serviceIterator.next();
    var10 = null;
}

当存在服务的时候对前面的几个参数(var9, var10)进行赋值,并置空原参数。那么当前var9存在服务,var10为null。然后这个while的判断为

while(!var9.supportsParameter(var3));

直接从方法名可以知道,当服务支持密钥的时候就退出。

然后第二个判断的函数名称为canUseProvider,可知当该服务可用的时候退出。然后

var11 = getTransform(var9, this.transforms);

可知根据服务和密钥信息获取密钥列表中和该服务对应的密钥并赋值给var11,目前为止这三个参数(var9, var10, var11)的赋值和我之前的猜测吻合。 那么后续的while判断都是获取和服务对应的密钥信息。

然后查看try-catch语句

if (var10 == null) {
    var10 = (CipherSpi)var9.newInstance((Object)null);
}

如果var19(CipherSpi)为null,则通过服务返回一个新的实例。该方法的描述为

Return a new instance of the implementation described by this service. The security provider framework uses this method to construct implementations. Applications will typically not need to call it.

返回此服务描述的实现的新实例。 安全提供程序框架使用此方法构造实现。 应用程序通常不需要调用它。

而CipherSpi这个类在文档中的描述为

This class defines the Service Provider Interface (SPI) for the Cipher class. All the abstract methods in this class must be implemented by each cryptographic service provider who wishes to supply the implementation of a particular cipher algorithm.

此类定义Cipher类的服务提供者接口(SPI)。 此类中的所有抽象方法必须由希望提供特定密码算法实现的每个加密服务提供者实现。

对此后面还有更加具体的描述

A transformation is a string that describes the operation (or set of operations) to be performed on the given input, to produce some output. A transformation always includes the name of a cryptographic algorithm (e.g., AES), and may be followed by a feedback mode and padding scheme.

转换是一个字符串,它描述要对给定输入执行的操作(或操作集),以产生一些输出。 变换总是包括加密算法的名称(例如,AES),并且可以跟随反馈模式和填充方案。

即这个类才是真正对数据进行操作的类。它提供对应服务的相关加密解密接口。

最后

    var11.setModePadding(var10);
    this.initCryptoPermission();
    this.implInit(var10, var1, var2, var3, var4, var5, var6);
    this.provider = var9.getProvider();
    this.spi = var10;
    this.firstService = null;
    this.serviceIterator = null;
    this.transforms = null;
    return;

将接口装入服务中,初始化加密权限,判断服务提供者接口和密钥库是否配对并将加密模式(1),密钥和安全的随机数生成器装入SPI中。并设置相关的参数。以及置空原参数。

按字节数组进行加密

首先查看方法体

public final byte[] doFinal(byte[] var1, int var2, int var3) throws IllegalBlockSizeException, BadPaddingException {
    this.checkCipherState();
    if (var1 != null && var2 >= 0 && var3 <= var1.length - var2 && var3 >= 0) {
        this.chooseFirstProvider();
        return this.spi.engineDoFinal(var1, var2, var3);
    } else {
        throw new IllegalArgumentException("Bad arguments");
    }
}

首先是

this.checkCipherState();

判断状态,如果不是加密或解密状态则抛出异常

然后是

    if (var1 != null && var2 >= 0 && var3 <= var1.length - var2 && var3 >= 0) {
        this.chooseFirstProvider();
        return this.spi.engineDoFinal(var1, var2, var3);
    } else {
        throw new IllegalArgumentException("Bad arguments");
    }

这里有个chooseFirstProvider选择首个服务提供商的方法,由于当前已经存在着服务提供商所以可以跳过。

最终他是返回SPI进行加密后的数据。这里我最终确定了数据是在SPI中处理的。

那么我们进入到engineDoFinal方法中一探究竟

SPI-engineDoFinal

进入方法,通过查看对该接口的实现我们可以发现多种算法的实现,这里我们选择当前Demo中的实现,即RSACipher

进入之后查看方法体

protected byte[] engineDoFinal(byte[] var1, int var2, int var3) throws BadPaddingException, IllegalBlockSizeException {
    this.update(var1, var2, var3);
    return this.doFinal();
}

这里有两个操作,分别是update和doFinal,这里我猜测一个是更新数据,另一个才是进行计算。

首先查看update

private void update(byte[] var1, int var2, int var3) {
    if (var3 != 0 && var1 != null) {
        if (this.bufOfs + var3 > this.buffer.length) {
            this.bufOfs = this.buffer.length + 1;
        } else {
            System.arraycopy(var1, var2, this.buffer, this.bufOfs, var3);
            this.bufOfs += var3;
        }
    }
}

在这之前有一个init方法会初始化密钥长度,而RSA的密钥长度最低为12bytes,具体可以查看关于RSA算法密钥长度/密文长度/明文长度。所以加密长度为117。 然后看doFinal

    private byte[] doFinal() throws BadPaddingException, IllegalBlockSizeException {
        if (this.bufOfs > this.buffer.length) {
            throw new IllegalBlockSizeException("Data must not be longer than " + this.buffer.length + " bytes");
        } else {
            try {
                byte[] var1;
                byte[] var2;
                byte[] var3;
                switch(this.mode) {
                case 1:
                    var1 = this.padding.pad(this.buffer, 0, this.bufOfs);
                    var3 = RSACore.rsa(var1, this.publicKey);
                    return var3;
                case 2:
                    var3 = RSACore.convert(this.buffer, 0, this.bufOfs);
                    var1 = RSACore.rsa(var3, this.privateKey, false);
                    byte[] var4 = this.padding.unpad(var1);
                    return var4;
                case 3:
                    var1 = this.padding.pad(this.buffer, 0, this.bufOfs);
                    var2 = RSACore.rsa(var1, this.privateKey, true);
                    return var2;
                case 4:
                    var2 = RSACore.convert(this.buffer, 0, this.bufOfs);
                    var1 = RSACore.rsa(var2, this.publicKey);
                    var3 = this.padding.unpad(var1);
                    return var3;
                default:
                    throw new AssertionError("Internal error");
                }
            } finally {
                this.bufOfs = 0;
            }
        }
    }

有init可知,当密钥为私钥的时候mode为3,所以执行的如下语句

var1 = this.padding.pad(this.buffer, 0, this.bufOfs);
var2 = RSACore.rsa(var1, this.privateKey, true);
return var2;

先填充var1获取要要加密的数据,然后在RSACore.rsa中进行加密。

最终加密函数为

    private static byte[] crtCrypt(byte[] var0, RSAPrivateCrtKey var1, boolean var2) throws BadPaddingException {
        BigInteger var3 = var1.getModulus();
        BigInteger var4 = parseMsg(var0, var3);
        BigInteger var6 = var1.getPrimeP();
        BigInteger var7 = var1.getPrimeQ();
        BigInteger var8 = var1.getPrimeExponentP();
        BigInteger var9 = var1.getPrimeExponentQ();
        BigInteger var10 = var1.getCrtCoefficient();
        BigInteger var11 = var1.getPublicExponent();
        BigInteger var12 = var1.getPrivateExponent();
        RSACore.BlindingRandomPair var13 = getBlindingRandomPair(var11, var12, var3);
        BigInteger var5 = var4.multiply(var13.u).mod(var3);
        BigInteger var14 = var5.modPow(var8, var6);
        BigInteger var15 = var5.modPow(var9, var7);
        BigInteger var16 = var14.subtract(var15);
        if (var16.signum() < 0) {
            var16 = var16.add(var6);
        }

        BigInteger var17 = var16.multiply(var10).mod(var6);
        BigInteger var18 = var17.multiply(var7).add(var15);
        var18 = var18.multiply(var13.v).mod(var3);
        if (var2 && !var4.equals(var18.modPow(var11, var3))) {
            throw new BadPaddingException("RSA private key operation failed");
        } else {
            return toByteArray(var18, getByteLength(var3));
        }
    }

这些涉及RSA对于java的实现以及一定的理论知识为基础,我太菜了现在有点绕晕了,所以暂时就到这里了。