XEcdhTest.java
package org.jose4j.jwe;
import org.jose4j.jca.ProviderContextTest;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.OctetKeyPairJsonWebKey;
import org.jose4j.jwk.OkpJwkGenerator;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwx.HeaderParameterNames;
import org.jose4j.jwx.Headers;
import org.jose4j.keys.AesKey;
import org.jose4j.keys.XDHKeyUtil;
import org.jose4j.lang.JoseException;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import javax.crypto.KeyAgreement;
import java.security.PublicKey;
import java.security.interfaces.XECPrivateKey;
import static org.junit.Assert.*;
public class XEcdhTest
{
@BeforeClass
public static void check()
{
// skip these tests if XDH isn't available (before java 11 I think)
org.junit.Assume.assumeTrue(new XDHKeyUtil().isAvailable());
}
@Test
public void rfc8037appendixA6() throws Exception
{
// https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.6
String jwkJsonToEncryptTo = "{\"kty\":\"OKP\",\"crv\":\"X25519\",\"kid\":\"Bob\",\n" +
" \"x\":\"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08\"}";
PublicJsonWebKey recipientPublicJwk = PublicJsonWebKey.Factory.newPublicJwk(jwkJsonToEncryptTo);
String ephemeralPublicJwkJson = "{\"kty\":\"OKP\",\"crv\":\"X25519\",\n" +
" \"x\":\"hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo\"}";
PublicJsonWebKey ephemeralPublicJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralPublicJwkJson);
byte[] ephemeralPrivateKeyBytes = new byte[] {119, 7, 109, 10, 115, 24, -91, 125, 60, 22, -63, 114, 81,
-78, 102, 69, -33, 76, 47, -121, -21, -64, -103, 42, -79, 119, -5, -91, 29, -71, 44, 42};
XDHKeyUtil keyUtil = new XDHKeyUtil();
XECPrivateKey ephemeralPrivateKey = keyUtil.privateKey(ephemeralPrivateKeyBytes, XDHKeyUtil.X25519);
KeyAgreement xka = KeyAgreement.getInstance("XDH");
xka.init(ephemeralPrivateKey);
PublicKey recipientPublicKey = recipientPublicJwk.getPublicKey();
xka.doPhase(recipientPublicKey, true);
byte[] dhz = xka.generateSecret();
byte[] expectedZ = new byte[] {74, 93, -99, 91, -92, -50, 45, -31, 114, -114, 59, -12, -128, 53, 15, 37,
-32, 126, 33, -55, 71, -47, -98, 51, 118, -16, -101, 60, 30, 22, 23, 66};
Assert.assertArrayEquals(expectedZ, dhz);
PublicJsonWebKey ephemeralJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralPublicJwkJson);
ephemeralJwk.setPrivateKey(ephemeralPrivateKey);
String ephemeralBothJwkJson = ephemeralJwk.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
PublicJsonWebKey parsedAgainEphemeralBothJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralBothJwkJson);
Assert.assertEquals(parsedAgainEphemeralBothJwk.getPrivateKey(), ephemeralPrivateKey);
Assert.assertEquals(parsedAgainEphemeralBothJwk.getPublicKey(), ephemeralPublicJwk.getPublicKey());
PublicJsonWebKey recipientPublicJwkAgain = PublicJsonWebKey.Factory.newPublicJwk(recipientPublicKey);
Assert.assertEquals(recipientPublicJwkAgain.getPublicKey(), recipientPublicKey);
Headers headers = new Headers();
headers.setStringHeaderValue(HeaderParameterNames.ALGORITHM, KeyManagementAlgorithmIdentifiers.ECDH_ES);
headers.setStringHeaderValue(HeaderParameterNames.ENCRYPTION_METHOD, ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
headers.setJwkHeaderValue(HeaderParameterNames.EPHEMERAL_PUBLIC_KEY, ephemeralJwk);
EcdhKeyAgreementAlgorithm ecdhKeyAgreementAlgorithm = new EcdhKeyAgreementAlgorithm();
ContentEncryptionKeyDescriptor cekDesc = new ContentEncryptionKeyDescriptor(32, AesKey.ALGORITHM);
PublicKey pubKey = recipientPublicJwk.getPublicKey();
ContentEncryptionKeys contentEncryptionKeys = ecdhKeyAgreementAlgorithm.manageForEncrypt(pubKey, cekDesc, headers, ephemeralJwk, ProviderContextTest.EMPTY_CONTEXT);
byte[] contentEncryptionKey = contentEncryptionKeys.getContentEncryptionKey();
assertEquals(32, contentEncryptionKey.length);
// this is the result of the kdf run on expectedZ
byte[] expectedDerivedKey = new byte[] {1, 58, 82, 48, 107, 105, 26, -77, 101, 111, -5, 111, -25, -69, 63, 6,
-87, -16, 37, 3, 90, -96, -91, -38, -26, 29, -120, 87, -68, 99, -11, -6};
assertArrayEquals(expectedDerivedKey, contentEncryptionKey);
}
@Test
public void rfc8037appendixA7() throws Exception
{
// https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A.7
String jwkJsonToEncryptTo = "{\"kty\":\"OKP\",\"crv\":\"X448\",\"kid\":\"Dave\",\n" +
" \"x\":\"PreoKbDNIPW8_AtZm2_sz22kYnEHvbDU80W0MCfYuXL8PjT7QjKhPKcG3LV67D2uB73BxnvzNgk\"}";
PublicJsonWebKey recipientPublicJwk = PublicJsonWebKey.Factory.newPublicJwk(jwkJsonToEncryptTo);
String ephemeralPublicJwkJson = "{\"kty\":\"OKP\",\"crv\":\"X448\",\n" +
" \"x\":\"mwj3zDG34-Z9ItWuoSEHSic70rg94Jxj-qc9LCLF2bvINmRyQdlT1AxbEtqIEg1TF3-A5TLEH6A\"}";
PublicJsonWebKey ephemeralPublicJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralPublicJwkJson);
byte[] ephemeralPrivateKeyBytes = new byte[] {-102, -113, 73, 37, -47, 81, -97, 87, 117, -49, 70, -80, 75,
88, 0, -44, -18, -98, -24, -70, -24, -68, 85, 101, -44, -104, -62, -115, -39, -55, -70, -11, 116,
-87, 65, -105, 68, -119, 115, -111, 0, 99, -126, -90, -15, 39, -85, 29, -102, -62, -40, -64, -91,
-104, 114, 107};
XDHKeyUtil keyUtil = new XDHKeyUtil();
XECPrivateKey ephemeralPrivateKey = keyUtil.privateKey(ephemeralPrivateKeyBytes, XDHKeyUtil.X448);
KeyAgreement xka = KeyAgreement.getInstance("XDH");
xka.init(ephemeralPrivateKey);
PublicKey recipientPublicKey = recipientPublicJwk.getPublicKey();
xka.doPhase(recipientPublicKey, true);
byte[] dhz = xka.generateSecret();
byte[] expectedZ = new byte[] {7, -1, -12, 24, 26, -58, -52, -107, -20, 28, 22, -87, 74, 15, 116, -47, 45,
-94, 50, -50, 64, -89, 117, 82, 40, 29, 40, 43, -74, 12, 11, 86, -3, 36, 100, -61, 53, 84, 57, 54,
82, 28, 36, 64, 48, -123, -43, -102, 68, -102, 80, 55, 81, 74, -121, -99};
Assert.assertArrayEquals(expectedZ, dhz);
PublicJsonWebKey ephemeralJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralPublicJwkJson);
ephemeralJwk.setPrivateKey(ephemeralPrivateKey);
String ephemeralBothJwkJson = ephemeralJwk.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
PublicJsonWebKey parsedAgainEphemeralBothJwk = PublicJsonWebKey.Factory.newPublicJwk(ephemeralBothJwkJson);
Assert.assertEquals(parsedAgainEphemeralBothJwk.getPrivateKey(), ephemeralPrivateKey);
Assert.assertEquals(parsedAgainEphemeralBothJwk.getPublicKey(), ephemeralPublicJwk.getPublicKey());
PublicJsonWebKey recipientPublicJwkAgain = PublicJsonWebKey.Factory.newPublicJwk(recipientPublicKey);
Assert.assertEquals(recipientPublicJwkAgain.getPublicKey(), recipientPublicKey);
Headers headers = new Headers();
headers.setStringHeaderValue(HeaderParameterNames.ALGORITHM, KeyManagementAlgorithmIdentifiers.ECDH_ES);
headers.setStringHeaderValue(HeaderParameterNames.ENCRYPTION_METHOD, ContentEncryptionAlgorithmIdentifiers.AES_256_CBC_HMAC_SHA_512);
headers.setJwkHeaderValue(HeaderParameterNames.EPHEMERAL_PUBLIC_KEY, ephemeralJwk);
EcdhKeyAgreementAlgorithm ecdhKeyAgreementAlgorithm = new EcdhKeyAgreementAlgorithm();
ContentEncryptionKeyDescriptor cekDesc = new ContentEncryptionKeyDescriptor(64, AesKey.ALGORITHM);
PublicKey pubKey = recipientPublicJwk.getPublicKey();
ContentEncryptionKeys contentEncryptionKeys = ecdhKeyAgreementAlgorithm.manageForEncrypt(pubKey, cekDesc, headers, ephemeralJwk, ProviderContextTest.EMPTY_CONTEXT);
byte[] contentEncryptionKey = contentEncryptionKeys.getContentEncryptionKey();
assertEquals(64, contentEncryptionKey.length);
// this is the result of the kdf run on expectedZ
byte[] expectedDerivedKey = new byte[] {48, 93, -30, -109, 3, -108, -2, -5, -114, -25, -119, -47, 12, -73,
-63, 85, -12, -53, -14, -22, 7, -62, 56, 96, -99, 13, -120, -124, -4, 26, -99, -57, 2, -116, 109,
73, -54, 80, -88, -77, 123, -68, -49, -112, -122, 34, 63, -20, 127, 69, -51, -68, 62, -19, 68, 106,
3, -26, -24, -10, 122, 120, -27, -40};
assertArrayEquals(expectedDerivedKey, contentEncryptionKey);
}
@Test
public void extraCookbook() throws JoseException
{
// example content from
// https://github.com/ietf-jose/cookbook/blob/master/curve25519/ecdh-es.json
String jwkJson =
"{\n" +
" \"kty\": \"OKP\",\n" +
" \"kid\": \"Bob\",\n" +
" \"use\": \"enc\",\n" +
" \"crv\": \"X25519\",\n" +
" \"x\": \"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08\",\n" +
" \"d\": \"XasIfmJKikt54X-Lg4AO5m87sSkmGLb9HC-LJ_-I4Os\"\n" +
"}";
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(jwkJson);
String jweString =
"eyJhbGciOiJFQ0RILUVTIiwia2lkIjoiQm9iIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJ" +
"4dlhIaDNRMlM0Vm90WGdCZGRHV0RfS2huellaX2V1QjJlYWVrTHZ2QURVIn0sImVuYyI6IkExMjhHQ00ifQ..0tCoBvRfolezYjpJ." +
"cX3qZ4cfxiyr_Leem1b69MeLo-BHsMJy6RetGse91pgXjR7X87k1e7dJliYfzgseMV9dcVo3i1cAPeU2jkDCxrjxIBW0iUlF3JfkVH7" +
"DU3B8GxzkblOJKwJE58Sd0rJEN9rSwfLf66sbTiY_vGYf7cZhbWhj6wwBnICFfaRh_2NubCdma7zX_vsdaGJXHn-6-jjR9UaQUbJD-t" +
"Pn5UAW8Sa9lPAcncYZywscUM-FET3tePOH2h_xv_LFGuN2KYJ3BED_Eyo--oSX17DD7ksSt0wfzy_TWczaG1E_TWn4nvP6T6d5nMp9O" +
"dfLWQNopQHoQGWlPbaRLHCk2mNl1K5GsJ6Q4tjSdFWpqRDpQUz2eYAk" +
".Yd4jUyp_PC5cACOwaAGwUQ";
String expected = "You can trust us to stick with you through thick and thin���to the bitter end." +
" And you can trust us to keep any secret of yours���closer than you keep it yourself." +
" But you cannot trust us to let you face trouble alone, and go off without a word." +
" We are your friends, Frodo.";
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setKey(jwk.getPrivateKey());
jwe.setCompactSerialization(jweString);
assertEquals(expected, jwe.getPayload());
}
@Test
public void roundTripJweX25519() throws Exception
{
OctetKeyPairJsonWebKey recipientJwk = OkpJwkGenerator.generateJwk(OctetKeyPairJsonWebKey.SUBTYPE_X25519);
System.out.println(recipientJwk.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE));
JsonWebEncryption jweObj = new JsonWebEncryption();
jweObj.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES);
jweObj.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
jweObj.setPayload("A donut with no hole is a danish");
jweObj.setKey(recipientJwk.getPublicKey());
String jwe = jweObj.getCompactSerialization();
System.out.println(jwe);
jweObj = new JsonWebEncryption();
jweObj.setKey(recipientJwk.getPrivateKey());
jweObj.setCompactSerialization(jwe);
String payload = jweObj.getPayload();
assertEquals("A donut with no hole is a danish", payload);
}
@Test
public void roundTripJweX448() throws Exception
{
OctetKeyPairJsonWebKey recipientJwk = OkpJwkGenerator.generateJwk(OctetKeyPairJsonWebKey.SUBTYPE_X448);
JsonWebEncryption jweObj = new JsonWebEncryption();
jweObj.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES_A256KW);
jweObj.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_256_CBC_HMAC_SHA_512);
jweObj.setPayload("Whoa, did somebody step on a duck?");
jweObj.setKey(recipientJwk.getPublicKey());
String jwe = jweObj.getCompactSerialization();
System.out.println(jwe);
jweObj = new JsonWebEncryption();
jweObj.setKey(recipientJwk.getPrivateKey());
jweObj.setCompactSerialization(jwe);
String payload = jweObj.getPayload();
assertEquals("Whoa, did somebody step on a duck?", payload);
}
@Test
public void consumeProducedElsewhere() throws JoseException
{
// JWE created using Nimbus which uses Tink for XDH
String jweString = "eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6Ii1Pbm9qa0t0X2VjMWVYX2FXTkx3RG5" +
"ndFhJRDBWNHJpbE5nZjRUYW1vbWMifSwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImFsZyI6IkVDREgtRVMifQ..n0a0IzC6I" +
"9O112_8_fq_RA.1ERfdkKoltne7Un7eEI8jA.c1z4C6K_9PrYoq9iYwTckg";
String jwkJson = "{\"kty\":\"OKP\",\"d\":\"zybXOI1wLTDCz751YDh_vL_U94IHQnswlShdEkPDsJU\"," +
"\"crv\":\"X25519\",\"x\":\"IZmhuDcUtycL0hFFpoVQ-iCje4RWZCfnuclMayw_KQQ\"}";
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(jwkJson);
JsonWebEncryption jwe = new JsonWebEncryption();
jwe.setKey(jwk.getPrivateKey());
jwe.setCompactSerialization(jweString);
assertEquals("meh", jwe.getPlaintextString());
}
}