Search code examples
java3d-securejwejose4jecdh

ECDH + JWE encryption/decryption using jose4j


I am trying to implement ECDH encryption/decryption along with JWE in Android (Java).
I have found the jose4j and Nimbus JOSE libraries that aim to do everything I need but appears that it's more challenging than I thought.

If anybody is familiar then it's for 3D Secure 2.0...

In the spec below:

  • SDK = the local side
  • DS = Directory Server (the other side)

Next is the spec:

  • Given: P(DS) - an EC public key (provided in PEM format, can be transformed to PublicKey or to JWK)
  • Generate a fresh ephemeral key pair (Q(SDK), d(SDK))
  • Conduct a Diffie-Hellman key exchange process according to JWA (RFC7518) in Direct Key Agreement mode using curve P-256, d(SDK) and P(DS) to produce a CEK. The parameter values supported in this version of the specification are:
    • "alg":ECDH-ES
    • "apv":DirectoryServerID
    • "epk":P(DS),inJSONWebKey(JWK)format {"kty":"EC", "crv":"P-256"}
    • All other parameters: not present
  • CEK:"kty":oct-256bits
  • Generate 128-bit random data as IV
  • Encrypt the JSON object according to JWE (RFC7516) using the CEK and JWE Compact Serialization. The parameter values supported in this version of the specification are:
    • "alg":dir
    • "epk":Q(SDK) as {"kty": "EC", "crv": "P-256"}
    • "enc":either"A128CBC-HS256"or"A128GCM"
    • All other parameters: not present
  • If the algorithm is A128CBC-HS256 use the full CEK or if the algorithm is A128GCM use the leftmost 128 bits of the CEK.
  • Delete the ephemeral key pair (Q(SDK),d(SDK))
  • Makes the resulting JWE available to the 3DS Server as SDK Encrypted Data

If someone has implemented this exact spec and can share the code this would be brilliant!!

There's an example of creating JWT using ECDH in the examples of jose4j:
https://bitbucket.org/b_c/jose4j/wiki/JWT%20Examples (the last example, titled as "Producing and consuming a nested (signed and encrypted) JWT").
But this example is not exactly what I need. It creates a token while I need to encrypt a text.

Starting from "CEK:"kty":oct-256bits" in the spec above I don't understand what to do.

Here's my code (so far) using Nimbus lib:

public String nimbus_encrypt(String plainJson, ECPublicKey otherPublicKey, String directoryServerId) throws JOSEException {
    JWEHeader jweHeader = new JWEHeader(
            JWEAlgorithm.ECDH_ES,
            EncryptionMethod.A128CBC_HS256,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            Base64URL.encode(directoryServerId),
            null,
            0,
            null,
            null,
            null,
            null);

    JWEObject jwe = new JWEObject(jweHeader, new Payload(plainJson));
    jwe.encrypt(new ECDHEncrypter(otherPublicKey));
    String serializedJwe = jwe.serialize();

    Log.d("[ENCRYPTION]", "nimbus_encrypt: jwe = " + jwe.getHeader());
    Log.d("[ENCRYPTION]", "nimbus_encrypt: serializedJwe = " + serializedJwe);

    return serializedJwe;
}

This is the nimbus output:

nimbus_encrypt: jwe = {"epk":{"kty":"EC","crv":"P-256","x":"AS0GRfAOWIDONXxaPR_4IuNHcDIUJPHbACjG5L7x-nQ","y":"xonFn1vRASKUTdCkFTwsl16LRmSe-bAF8EO4-mh1NYw"},"apv":"RjAwMDAwMDAwMQ","enc":"A128CBC-HS256","alg":"ECDH-ES"}

nimbus_encrypt: serializedJwe = eyJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJBUzBHUmZBT1dJRE9OWHhhUFJfNEl1TkhjRElVSlBIYkFDakc1TDd4LW5RIiwieSI6InhvbkZuMXZSQVNLVVRkQ2tGVHdzbDE2TFJtU2UtYkFGOEVPNC1taDFOWXcifSwiYXB2IjoiUmpBd01EQXdNREF3TVEiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiRUNESC1FUyJ9..Pi48b7uj3UilvVXKewFacg.0sx9OkHxxtZvkVm-IENRFw.bu5GvOAwcZxdxaDKWIBqwA

Here's my code (so far, using @Brian-Campbell's answer) using jose4j lib:

public String jose4j_encrypt(String plainJson, PublicKey otherPublicKey, String directoryServerId) throws JoseException {
    JsonWebEncryption jwe = new JsonWebEncryption();
    jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES);
    jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);

    jwe.setHeader(HeaderParameterNames.AGREEMENT_PARTY_V_INFO, Base64Url.encodeUtf8ByteRepresentation(directoryServerId));
    jwe.setKey(otherPublicKey);
    jwe.setPayload(plainJson);

    String serializedJwe = jwe.getCompactSerialization();
    Log.d("[ENCRYPTION]", "jose4j_encrypt: jwe = " + jwe);
    Log.d("[ENCRYPTION]", "jose4j_encrypt: serializedJwe = " + serializedJwe);

    return serializedJwe;
}

This is the jose4j output:

jose4j_encrypt: jwe = JsonWebEncryption{"alg":"ECDH-ES","enc":"A128CBC-HS256","apv":"RjAwMDAwMDAwMQ","epk":{"kty":"EC","x":"prvyhexJXDWvPQmPA1xBjY8mkHEbrEiJ4Dr-7_5YfdQ","y":"fPjw8UdfzgkVTppPSN5o_wprItKLwecoia9yrWi38yo","crv":"P-256"}}

jose4j_encrypt: serializedJwe = eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImFwdiI6IlJqQXdNREF3TURBd01RIiwiZXBrIjp7Imt0eSI6IkVDIiwieCI6InBydnloZXhKWERXdlBRbVBBMXhCalk4bWtIRWJyRWlKNERyLTdfNVlmZFEiLCJ5IjoiZlBqdzhVZGZ6Z2tWVHBwUFNONW9fd3BySXRLTHdlY29pYTl5cldpMzh5byIsImNydiI6IlAtMjU2In19..gxWYwFQSOqLk5HAgs7acdA.mUIHBiWpWSlQaEOJ_EZGYA.eiTe-88fw-Jfuhji_W0rtg

As can be seen the "alg" header in the final result is "ECDH-ES" and not "dir" as required.

If I would implement both sides of the communication then it would be enough, but with this spec seems like many configurations are missing here...

Code using jose4j is longer and seems more configurable but I couldn't construct something valuable enough to post here.

The main missing part for me is how to generate the CEK from the spec above.

Thank you.

EDIT
Added jose4j code above and added the outputs...


Solution

  • Below is some example code using jose4j that I think does what you're looking for. The example you pointed to is similar with the plaintext of the JWE being a JWS/JWT but it can be any arbitrary content. The details of the CEK generation/derivation are taken care of by the underlying JWE functionality. Please note that this only encrypts the content and doesn't provide integrity protection or sender authentication.

        String encodedCert = "MIIBRjCB7KADAgECAgYBaqxRCjswDAYIKoZIzj0EAwIFADApMQswCQYDVQQGEwJDQTEMMAoGA1UE\n" +
                "ChMDbWVoMQwwCgYDVQQDEwNtZWgwHhcNMTkwNTEyMTM1MjMzWhcNMjAwNTExMTM1MjMzWjApMQsw\n" +
                "CQYDVQQGEwJDQTEMMAoGA1UEChMDbWVoMQwwCgYDVQQDEwNtZWgwWTATBgcqhkjOPQIBBggqhkjO\n" +
                "PQMBBwNCAAQH83AhYHCehKj7M5+UTNshwLFqqqJWGrJPNj9Kr7xvxtcZnyjq+AKLGMLfdk/G7yb8\n" +
                "4vIh0cJwtVs70WgIXT8xMAwGCCqGSM49BAMCBQADRwAwRAIgO0PJRzan2msHpcvcqhybzeualDea\n" +
                "/X2QGAWCYT+sNiwCIDMrfhrzUQ6uIX4vnB8AYqb85Ssl7Qcl9nYtjHb08NR8";
    
        X509Util x509Util = new X509Util();
        X509Certificate x509Certificate = x509Util.fromBase64Der(encodedCert);
    
        // the JWE object
        JsonWebEncryption jwe = new JsonWebEncryption();
    
        // The output of the ECDH-ES key agreement will be used as the content encryption key
        jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.ECDH_ES);
    
        // The content encryption key is used to encrypt the payload
        // with a composite AES-CBC / HMAC SHA2 encryption algorithm
        jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
    
        // don't think you really need this but you had ""apv":DirectoryServerID" in the question so...
        jwe.setHeader(HeaderParameterNames.AGREEMENT_PARTY_V_INFO, Base64Url.encodeUtf8ByteRepresentation("<<DirectoryServerID>>"));
    
        // We encrypt to the receiver using their public key
        jwe.setKey(x509Certificate.getPublicKey());
    
        // and maybe put x5t to help the receiver know which key to use in decryption
        jwe.setX509CertSha1ThumbprintHeaderValue(x509Certificate);
    
        // What is going to be encrypted
        jwe.setPayload("Your text here. It can be JSON or whatever.");
    
        // Produce the JWE compact serialization, which is a string consisting of five dot ('.') separated
        // base64url-encoded parts in the form Header..IV.Ciphertext.AuthenticationTag
        String serializedJwe = jwe.getCompactSerialization();
    
        System.out.println(serializedJwe);