Java Cipher初探

dust1發表於2019-02-01

在編寫專案的時候由於要使用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

Java Cipher初探

進入之後檢視方法體

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的實現以及一定的理論知識為基礎,我太菜了現在有點繞暈了,所以暫時就到這裡了。

相關文章