4

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...

Hugues M.
  • 19,846
  • 6
  • 37
  • 65
  • 1
    The good news: all the information is in there as metadata, somewhere. The bad news: for elliptic curves at least the same curve may be specified in different ways, so there isn't a canonical one-and-only representation for a given elliptic curve. As for parsing out the metadata, if you are probably better off using a proper ASN.1 parsing library like for example Bouncycastle, but it can also be done "by hand" in the manner you are doing if you are careful. Finally, there are non-supported `DerThisAndThat` classes in the `sun.*` namespace you can use if you are a hateful person. – President James K. Polk Jul 12 '19 at 17:34
  • 1
    I wonder if it isn't easier just to speculatively create the key using the `KeyFactory.generatePrivate()` for one key type, then catch the appropriate exceptions and try the next key type until `generatePrivate()` succeeds. Then the actual `PrivateKey` instance can be probed to see what it is. – President James K. Polk Jul 12 '19 at 17:39
  • Thanks, I've considered parsing ASN.1 but I'd also need to implement some PKCS#8 logic on top, that I might get wrong. Trying all standard algos would actually be a good idea! Is there a way to dynamically get the list of supported algos for KeyPairs, or do I have to hardcode it to `["EC", "RSA", "DSA", "DiffieHellman"]`? --Hmm maybe _that_ should be a distinct (and much simpler) question... – Hugues M. Jul 12 '19 at 21:29
  • So yeah there is, it will work fine for my need, but it's a shame that the JCA cannot do better than that. – Hugues M. Jul 12 '19 at 22:11

1 Answers1

4

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");
} 
Hugues M.
  • 19,846
  • 6
  • 37
  • 65
  • This is a great answer, allowing one to decode a public key without requiring Bouncy Castle. Although I would have thrown an InvalidKeySpecificationException, instead of a RuntimeException. It's neater that way. – SeverityOne Jun 18 '21 at 06:25