Aes128CbcHmacSha256ContentEncryptionAlgorithmTest.java

/*
 * Copyright 2012-2017 Brian Campbell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jose4j.jwe;

import org.jose4j.base64url.Base64Url;
import org.jose4j.jca.ProviderContextTest;
import org.jose4j.jwx.Headers;
import org.jose4j.lang.ByteUtil;
import org.jose4j.lang.JoseException;
import org.jose4j.lang.StringUtil;
import org.jose4j.lang.UncheckedJoseException;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.LoggerFactory;

import java.util.Arrays;

/**
 */
public class Aes128CbcHmacSha256ContentEncryptionAlgorithmTest
{
    @Test
    public void testExampleEncryptFromJweAppendix2() throws JoseException
    {
        // http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-13#appendix-A.2
        String plainTextText = "Live long and prosper.";
        byte[] plainText = StringUtil.getBytesUtf8(plainTextText);

        String encodedHeader = "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0" +
                "JDLUhTMjU2In0";
        Base64Url base64url = new Base64Url();
        Headers headers = new Headers();
        headers.setFullHeaderAsJsonString(base64url.base64UrlDecodeToUtf8String(encodedHeader));

        byte[] aad = StringUtil.getBytesAscii(encodedHeader);

        int[] ints = {4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207};
        byte[] contentEncryptionKeyBytes = ByteUtil.convertUnsignedToSignedTwosComp(ints);

        byte[] iv = ByteUtil.convertUnsignedToSignedTwosComp(new int[]{3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101});

        AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256 jweContentEncryptionAlg = new AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256();
        ContentEncryptionParts contentEncryptionParts = jweContentEncryptionAlg.encrypt(plainText, aad, contentEncryptionKeyBytes, iv, headers, ProviderContextTest.EMPTY_CONTEXT);

        Base64Url base64Url = new Base64Url();

        byte[] ciphertext = contentEncryptionParts.getCiphertext();
        String encodedJweCiphertext = "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY";
        Assert.assertEquals(encodedJweCiphertext, base64Url.base64UrlEncode(ciphertext));

        byte[] authenticationTag = contentEncryptionParts.getAuthenticationTag();
        String encodedAuthenticationTag = "9hH0vgRfYgPnAHOd8stkvw";
        Assert.assertEquals(encodedAuthenticationTag, base64Url.base64UrlEncode(authenticationTag));
    }

    @Test
    public void testExampleDecryptFromJweAppendix2() throws JoseException
    {
        int[] ints = {4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207};
        byte[] contentEncryptionKeyBytes = ByteUtil.convertUnsignedToSignedTwosComp(ints);

        Base64Url b = new Base64Url();

        String encodedHeader = "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0";
        Headers headers = new Headers();
        headers.setFullHeaderAsJsonString(Base64Url.decodeToUtf8String(encodedHeader));

        byte[] header = StringUtil.getBytesUtf8(encodedHeader);
        byte[] iv = b.base64UrlDecode("AxY8DCtDaGlsbGljb3RoZQ");
        byte[] ciphertext = b.base64UrlDecode("KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY");
        byte[] tag = b.base64UrlDecode("9hH0vgRfYgPnAHOd8stkvw");

        AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256 jweContentEncryptionAlg = new AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256();
        ContentEncryptionParts encryptionParts = new ContentEncryptionParts(iv, ciphertext, tag);
        byte[] plaintextBytes = jweContentEncryptionAlg.decrypt(encryptionParts, header, contentEncryptionKeyBytes, headers, ProviderContextTest.EMPTY_CONTEXT);

        Assert.assertEquals("Live long and prosper.", StringUtil.newStringUtf8(plaintextBytes));
    }

    @Test
    public void testRoundTrip() throws JoseException
    {
        String text = "I'm writing this test on a flight to Zurich";
        String encodedHeader = "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0";
        Headers headers = new Headers();
        headers.setFullHeaderAsJsonString(Base64Url.decodeToUtf8String(encodedHeader));
        byte[] aad = StringUtil.getBytesUtf8(encodedHeader);
        byte[] plaintext = StringUtil.getBytesUtf8(text);
        AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256 contentEncryptionAlg = new AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256();
        ContentEncryptionKeyDescriptor cekDesc = contentEncryptionAlg.getContentEncryptionKeyDescriptor();
        byte[] cek = ByteUtil.randomBytes(cekDesc.getContentEncryptionKeyByteLength());
        ContentEncryptionParts encryptionParts = contentEncryptionAlg.encrypt(plaintext, aad, cek, headers, null, ProviderContextTest.EMPTY_CONTEXT);

        byte[] decrypt = contentEncryptionAlg.decrypt(encryptionParts, aad, cek, null, ProviderContextTest.EMPTY_CONTEXT);
        Assert.assertEquals(text, StringUtil.newStringUtf8(decrypt));
    }

   @Ignore // don't run normally b/c it's slow and needs extra memory (for the actual bypass anyway)
   /*        i.e. in build ...
           <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <configuration>
            <argLine>-Xmx32760m</argLine>
          </configuration>
        </plugin>
    */
    @Test
    public void testIntegerOverflow() throws Exception {

        byte[] cek = new byte[] {57, -68, 52, 101, -57, -48, -121, 76, -97, 67, 65, 71, -60, -120, -119, 113,
                -29, -24, 28, 1, 61, -99, 73, -100, 68, 103, 67, -6, -41, -94, -75, -95};

        byte[] aad = new byte[8];
        byte[] plaintext = new byte[536870928];
        for (int i = 0; i < plaintext.length; i = i + 8)
        {
            // Doesn't matter what value is, but rand is too expensive for large array
            byte[] bytes = ByteUtil.getBytes(i);
            plaintext[i] = bytes[0];
            plaintext[i+1] = bytes[1];
            plaintext[i+2] = bytes[2];
            plaintext[i+3] = bytes[3];
        }

        Headers noopheaders = new Headers();

        AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256 cea = new AesCbcHmacSha2ContentEncryptionAlgorithm.Aes128CbcHmacSha256();

        ContentEncryptionParts encryptedParts = cea.encrypt(plaintext, aad, cek, noopheaders, null, ProviderContextTest.EMPTY_CONTEXT);

        byte[] ciphertext = encryptedParts.getCiphertext();
        byte[] authTag = encryptedParts.getAuthenticationTag();
        byte[] iv = encryptedParts.getIv();

        // Now shift aad and ciphertext around so that HMAC doesn't change,
        // but the plaintext will change.
        int n = 0;
        byte[] buffer = new byte[aad.length + iv.length + ciphertext.length];
        System.arraycopy(aad, 0, buffer, n, aad.length);
        n += aad.length;
        System.arraycopy(iv, 0, buffer, n, iv.length);
        n += iv.length;
        System.arraycopy(ciphertext, 0, buffer, n, ciphertext.length);
        // Note that due to integer overflow :536870920 * 8 = 64
        int newAadSize = 536870920;
        byte[] newAad = Arrays.copyOfRange(buffer, 0, newAadSize);
        byte[] newIv = Arrays.copyOfRange(buffer, newAadSize, newAadSize + 16);
        byte[] newCiphertext = Arrays.copyOfRange(buffer, newAadSize + 16, buffer.length);

        try
        {
            ContentEncryptionParts cep = new ContentEncryptionParts(newIv, newCiphertext, authTag);  // the authTag does NOT change.
            byte[] decrypt = cea.decrypt(cep, newAad, cek, noopheaders, ProviderContextTest.EMPTY_CONTEXT);

            // Reaching this point means that the HMAC check was bypassed although the decrypted data is different
            // from the original plaintext.
            Assert.fail("decrypt should have failed but " + Arrays.toString(decrypt));
        }
        catch (UncheckedJoseException e)
        {
            LoggerFactory.getLogger(this.getClass()).debug("This was expected: " + e);
        }
    }
}