Search code examples
javacryptographyasn.1pkcs#8

Extracting algorithm ID from bytes of encoded private key


Is it possible to create a PrivateKey from an encoded byte array alone, without knowing the algorithm in advance?

So in a way it's a twist on that question, that's not adressed in the answers.

Say I have a pair of keys generated this way:

KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); // Could be "EC" instead of "RSA"
String privateKeyB64 = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
writePrivateKeyToSafeLocation(privateKeyB64);

To obtain a PrivateKey from the base64-encoded bytes, I can do this, but I have to know the algorithm family in advance:

String privateKeyB64 = readPrivateKeyFromSafeLocation();
EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyB64));
byte[] encodedKeyBytes = encodedKeySpec.getEncoded();
String algorithmFamily = "RSA"; // Can this be deduced from encodedKeyBytes?
PrivateKey key = KeyFactory.getInstance(algorithmFamily).generatePrivate(encodedKeySpec);

Unfortunately encodedKeySpec.getAlgorithm() returns null.

I'm pretty sure the algorithm ID is actually specified within those bytes in PKCS#8 format, but I'm not sure how to read the ASN.1 encoding.

Can I "sniff" the algorithm ID in a reliable way from those bytes?

It's OK to only support RSA and EC (algorithms supported by the JRE, without additional providers).

To get an idea of what I'm after, here is an attempt that seems to work empirically:

private static final byte[] EC_ASN1_ID = {42, -122, 72, -50, 61, 2, 1};
private static final byte[] RSA_ASN1_ID = {42, -122, 72, -122, -9, 13, 1, 1, 1};
private static final int EC_ID_OFFSET = 9;
private static final int RSA_ID_OFFSET = 11;

private static String sniffAlgorithmFamily(byte[] keyBytes) {
    if (Arrays.equals(Arrays.copyOfRange(keyBytes, EC_ID_OFFSET, EC_ID_OFFSET + EC_ASN1_ID.length), EC_ASN1_ID)) {
        return "EC";
    }
    if (Arrays.equals(Arrays.copyOfRange(keyBytes, RSA_ID_OFFSET, RSA_ID_OFFSET + RSA_ASN1_ID.length), RSA_ASN1_ID)) {
        return "RSA";
    }
    throw new RuntimeException("Illegal key, this thingy requires either RSA or EC private key");
}

But I have no idea if that is safe to use. Maybe the IDs are not always at those offsets. Maybe they can be encoded in some other ways...


Solution

  • As suggested by James in comments, trying every supported algorithm would work, in a much safer way.

    It's possible to dynamically obtain the list of such algorithms:

    Set<String> supportedKeyPairAlgorithms() {
        Set<String> algos = new HashSet<>();
        for (Provider provider : Security.getProviders()) {
            for (Provider.Service service : provider.getServices()) {
                if ("KeyPairGenerator".equals(service.getType())) {
                    algos.add(service.getAlgorithm());
                }
            }
        }
        return algos;
    }
    

    And with that, just try them all to generate the KeyPair:

    PrivateKey generatePrivateKey(String b64) {
        byte[] bytes = Base64.getDecoder().decode(b64);
        for (String algorithm : supportedKeyPairAlgorithms()) {
            try {
                LOGGER.debug("Attempting to decode key as " + algorithm);
                return KeyFactory.getInstance(algorithm).generatePrivate(new PKCS8EncodedKeySpec(bytes));
            } catch (NoSuchAlgorithmException e) {
                LOGGER.warn("Standard algorithm " + algorithm + " not known by this Java runtime from outer space", e);
            } catch (InvalidKeySpecException e) {
                LOGGER.debug("So that key is not " + algorithm + ", nevermind", e);
            }
        }
        throw new RuntimeException("No standard KeyFactory algorithm could decode your key");
    }