Search code examples
javascriptjavacryptographybouncycastlewebcrypto

Java-Generated Private Key Imports in Chrome but Fails in Safari


I am working on a project where I generate an EC private key using Java and then import it in the browser using JavaScript. The key imports successfully in Chrome, but it fails in Safari.Here’s my JavaScript code for importing private key:

[Try running this html file in browser]

<!DOCTYPE html>
<html>
<head>
  <title>ECDH Key Pair Generation</title>
</head>
<body> 
  <script>

//Utils
function _extractRawKeyMaterial(pem, type) {
  const pemHeader = `-----BEGIN ${type} KEY-----`;
  const pemFooter = `-----END ${type} KEY-----`;

  const endingIndex = pem.indexOf(pemFooter);
  const startingIndex = pem.indexOf(pemHeader) + pemHeader.length;

  const pemContents = pem.substring(startingIndex, endingIndex);
  var return_object = convertBase64StringToArrayBuffer(pemContents.trim());
  return return_object;
}

 const convertBase64StringToArrayBuffer = base64String => {
  const text = window.atob(base64String);
  return convertStringToArrayBuffer(text);
};

 const convertStringToArrayBuffer = str => {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
};


// private key
var privateKeyGenerated = `-----BEGIN PRIVATE KEY-----
ME4CAQAwEAYHKoZIzj0CAQYFK4EEACIENzA1AgEBBDAMvyd7HU0FwJxgs5N87NVw
MPOR60umJXnhPjdtn0O0RHgx2J0sVnvw7B6ue1Wb5uQ=
-----END PRIVATE KEY-----`

// Pass the loaded private key to your function
_loadEccPrivateKey(privateKeyGenerated);

// Code working in chrome but fails in safari with an error : Data provided to an operation does not meet requirements
 async function _loadEccPrivateKey(pemKey) {
  try {
     const rawKey = _extractRawKeyMaterial(pemKey.trim(), "PRIVATE");

    //console.log(rawKey)
    const key = await window.crypto.subtle.importKey(
      "pkcs8", // Format for private keys
      rawKey,
      {
        name: "ECDH",
        namedCurve: "P-384",
      },
      true,
      ["deriveBits", "deriveKey"] // Key usages
    );

    console.log('Imported Private Key:', key);
    return key;
  } catch (e) {
    console.error('Error importing private key:', e);
    throw e;
  }
}

</script> 
</body>
</html>

The code works perfectly in Chrome but throws an error in Safari. The error message is "DATA PROVIDED TO AN OPERATION DOES NOT MEET REQUIREMENTS"

Here is my JAVA CODE for more information:


import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.FileOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;

public class TestApplication {

    private static final String CURVE = "secp384r1"; // P-384 curve

    public static void main(String[] args) {
        try {
            // Add BouncyCastle Provider
            Security.addProvider(new BouncyCastleProvider());

            // Generate EC key pair
            ECGenParameterSpec parameterSpec = new ECGenParameterSpec(CURVE);
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC");
            keyPairGenerator.initialize(parameterSpec, new SecureRandom());
            KeyPair keyPair = keyPairGenerator.generateKeyPair();

            // Extract and print private key
            PrivateKey privateKey = keyPair.getPrivate();
            String privateKeyPem = convertToPem(privateKey);
            System.out.println("Private Key in PEM format:\n" + privateKeyPem);

            // Save the private key in binary format to a file (optional)
            String privateKeyFilePath = "private_key.bin";
            saveKeyToBinaryFile(privateKey, privateKeyFilePath);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Convert private key to PEM format
    private static String convertToPem(PrivateKey privateKey) {
        String base64Key = Base64.getEncoder().encodeToString(privateKey.getEncoded());
        return "-----BEGIN PRIVATE KEY-----\n" +
                base64Key +
                "\n-----END PRIVATE KEY-----";
    }

    // Save the private key in binary format
    private static void saveKeyToBinaryFile(PrivateKey privateKey, String filePath) {
        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(privateKey.getEncoded());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


If you want to try it yourself, just run this Java POC: https://github.com/ChetanTailor/JavaPrivateKeyPOC


Solution

  • This is a known Safari and Firefox bug where importKey requires EC keys to include the public component as well as the private.

    Here's a working P-384 private key (generated with openssl ecparam -genkey -name prime256v1 -noout and ASCII armor tweaked to match the expected header):

    var privateKeyGenerated = `-----BEGIN PRIVATE KEY-----
    MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBoZCuF4gA0MozAQFtE
    lm+zCPikEs5JeMFyZRVPpXEHYsQQFZc71KYFNdAA0uazYHWhZANiAAQkQ/kYHu/y
    F9Ec2QPkQxtqRWKgi8U2ZIqo6SeJfgs/4g7P3EaFgx/T2BAGw1HIrwfO1kiAJi/f
    tkdHqte8uf88Oo8vq1YSniBNV8E4kC4VbsrHNrYcBPk0XfyL1B4pJ8M=
    -----END PRIVATE KEY-----`
    

    You can compare the ASN.1 parsing of this key:

    PrivateKeyInfo SEQUENCE (3 elem)
    
        version Version INTEGER 0
        privateKeyAlgorithm AlgorithmIdentifier SEQUENCE (2 elem)
            algorithm OBJECT IDENTIFIER 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type)
            parameters ANY OBJECT IDENTIFIER 1.3.132.0.34 secp384r1 (SECG (Certicom) named elliptic curve)
        privateKey PrivateKey OCTET STRING (158 byte) 30819B020101043068642B85E20034328CC0405B44966FB308F8A412CE4978C172651…
            SEQUENCE (3 elem)
                INTEGER 1
                OCTET STRING (48 byte) 68642B85E20034328CC0405B44966FB308F8A412CE4978C17265154FA5710762C41015…
                [1] (1 elem)
                    BIT STRING (776 bit) 0000010000100100010000111111100100011000000111101110111111110010000101…
    

    with the key you provided:

    PrivateKeyInfo SEQUENCE (3 elem)
    
        version Version INTEGER 0
        privateKeyAlgorithm AlgorithmIdentifier SEQUENCE (2 elem)
            algorithm OBJECT IDENTIFIER 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type)
            parameters ANY OBJECT IDENTIFIER 1.3.132.0.34 secp384r1 (SECG (Certicom) named elliptic curve)
        privateKey PrivateKey OCTET STRING (55 byte) 303502010104300CBF277B1D4D05C09C60B3937CECD57030F391EB4BA62579E13E376D…
            SEQUENCE (2 elem)
                INTEGER 1
                OCTET STRING (48 byte) 0CBF277B1D4D05C09C60B3937CECD57030F391EB4BA62579E13E376D9F43B4447831D8…
    

    Note that the second one is missing an element at the end, representing the public key.


    You can fix your Java code by passing the private key through PrivateKeyInfo, which is the ASN.1 structure expected by browsers. Unfortunately BouncyCastle's implementation introduces new, unsupported structures, like a public key identifier, so you must manually re-encode it with only the parts you want.

    This way you can create an encoded key that exactly matches the OpenSSL structures:

    PrivateKeyInfo originalKeyInfo = PrivateKeyInfo.getInstance(keyPair.getPrivate().getEncoded());
    
    ASN1Sequence oldPrivateKeySequence = DERSequence
            .getInstance(originalKeyInfo.getPrivateKey().getOctets());
    DERSequence newPrivateKeySequence = new DERSequence(new ASN1Encodable[] {
            // Version (1).
            oldPrivateKeySequence.getObjectAt(0),
    
            // Private key bytes.
            oldPrivateKeySequence.getObjectAt(1),
    
            // Public key algorithm. Accepted by Firefox but not Safari, so must be skipped.
            // oldPrivateKeySequence.getObjectAt(2),
    
            // Public key bytes, tagged [1].
            oldPrivateKeySequence.getObjectAt(3),
    });
    
    // Re-create PrivateKeyInfo with only the structures we want.
    ASN1EncodableVector v = new ASN1EncodableVector();
    
    // Version fixed to zero.
    v.add(new ASN1Integer(BigIntegers.ZERO));
    v.add(originalKeyInfo.getPrivateKeyAlgorithm());
    v.add(new DEROctetString(newPrivateKeySequence));
    
    byte[] keyPairEncoded = new DERSequence(v).getEncoded();
    

    Here's the full source code:

    import org.bouncycastle.asn1.ASN1Encodable;
    import org.bouncycastle.asn1.ASN1EncodableVector;
    import org.bouncycastle.asn1.ASN1Integer;
    import org.bouncycastle.asn1.ASN1Primitive;
    import org.bouncycastle.asn1.ASN1Sequence;
    import org.bouncycastle.asn1.ASN1Set;
    import org.bouncycastle.asn1.DEROctetString;
    import org.bouncycastle.asn1.DERSequence;
    import org.bouncycastle.asn1.DERTaggedObject;
    import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
    import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.util.BigIntegers;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.security.*;
    import java.security.spec.ECGenParameterSpec;
    import java.util.Base64;
    
    public class TestApplication {
    
        private static final String CURVE = "secp384r1"; // P-384 curve
    
        public static void main(String[] args) {
            try {
                // Add BouncyCastle Provider
                Security.addProvider(new BouncyCastleProvider());
    
                // Generate EC key pair
                ECGenParameterSpec parameterSpec = new ECGenParameterSpec(CURVE);
                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC");
                keyPairGenerator.initialize(parameterSpec, new SecureRandom());
                KeyPair keyPair = keyPairGenerator.generateKeyPair();
    
                // Encode with Safari-compatible ASN.1 structure.
                byte[] keyPairBytes = encodeKeyPair(keyPair);
    
                // Extract and print key pair
                String privateKeyPem = convertToPem(keyPairBytes);
                System.out.println("Private Key in PEM format:\n" + privateKeyPem);
    
                // Save the key pair in binary format to a file (optional)
                String privateKeyFilePath = "private_key.bin";
                saveKeyToBinaryFile(keyPairBytes, privateKeyFilePath);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        // Convert a KeyPair into ASN.1 encoded PrivateKeyInfo compatible with Safari.
        private static byte[] encodeKeyPair(KeyPair keyPair) throws IOException {
            PrivateKeyInfo originalKeyInfo = PrivateKeyInfo.getInstance(keyPair.getPrivate().getEncoded());
    
            ASN1Sequence oldPrivateKeySequence = DERSequence
                    .getInstance(originalKeyInfo.getPrivateKey().getOctets());
            DERSequence newPrivateKeySequence = new DERSequence(new ASN1Encodable[] {
                    // Version (1).
                    oldPrivateKeySequence.getObjectAt(0),
    
                    // Private key bytes.
                    oldPrivateKeySequence.getObjectAt(1),
    
                    // Public key algorithm. Accepted by Firefox but not Safari, so must be skipped.
                    // oldPrivateKeySequence.getObjectAt(2),
    
                    // Public key bytes, tagged [1].
                    oldPrivateKeySequence.getObjectAt(3),
            });
    
            // Re-create PrivateKeyInfo with only the structures we want.
            ASN1EncodableVector v = new ASN1EncodableVector();
    
            // Version fixed to zero.
            v.add(new ASN1Integer(BigIntegers.ZERO));
            v.add(originalKeyInfo.getPrivateKeyAlgorithm());
            v.add(new DEROctetString(newPrivateKeySequence));
    
            return new DERSequence(v).getEncoded();
        }
    
        // Convert private key to PEM format
        private static String convertToPem(byte[] privateKey) {
            String base64Key = Base64.getEncoder().encodeToString(privateKey);
            return "-----BEGIN PRIVATE KEY-----\n" +
                    base64Key +
                    "\n-----END PRIVATE KEY-----";
        }
    
        // Save the private key in binary format
        private static void saveKeyToBinaryFile(byte[] privateKey, String filePath) {
            try (FileOutputStream fos = new FileOutputStream(filePath)) {
                fos.write(privateKey);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    Finally, here's an example of the encoded keypair that this code generates:

    -----BEGIN PRIVATE KEY-----
    MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBzsru70B3wapVJZsFj4hUHxAGO4B5fJypfAvGyKEyRc2ZdjaVWIOd+vfhgfKFIqe6hZANiAAR7f1ZbUKI2lLAgZ4dnHVHGTQ7D9E2yMxwT5gYiGKdc8+AHGBzoYauI4YTOMVBYHwNrqYT1oO0ruH2sI53U+iy1KnbUAPAP9z0lHi8HONJZ8D+FbKTQa5LWihLTJLihFJw=
    -----END PRIVATE KEY-----
    

    You can see how it's parsed with the same structure as the first OpenSSL key:

    PrivateKeyInfo SEQUENCE (3 elem)
    
        version Version INTEGER 0
        privateKeyAlgorithm AlgorithmIdentifier SEQUENCE (2 elem)
            algorithm OBJECT IDENTIFIER 1.2.840.10045.2.1 ecPublicKey (ANSI X9.62 public key type)
            parameters ANY OBJECT IDENTIFIER 1.3.132.0.34 secp384r1 (SECG (Certicom) named elliptic curve)
        privateKey PrivateKey OCTET STRING (158 byte) 30819B020101043073B2BBBBD01DF06A954966C163E21507C4018EE01E5F272A5F02F…
            SEQUENCE (3 elem)
                INTEGER 1
                OCTET STRING (48 byte) 73B2BBBBD01DF06A954966C163E21507C4018EE01E5F272A5F02F1B2284C9173665D8D…
                [1] (1 elem)
                    BIT STRING (776 bit) 0000010001111011011111110101011001011011010100001010001000110110100101…