I'm trying to import various private keys into AndroidKeyStore. I'm using BouncyCastle to decode raw key data. While I am able to obtain usable KeyPairs, I can't put EC keys into AndroidKeyStore due to the following exception:

java.security.KeyStoreException: Unsupported key algorithm: ECDSA
    at android.security.keystore.AndroidKeyStoreSpi.getLegacyKeyProtectionParameter(AndroidKeyStoreSpi.java:348)
    at android.security.keystore.AndroidKeyStoreSpi.setPrivateKeyEntry(AndroidKeyStoreSpi.java:360)
    at android.security.keystore.AndroidKeyStoreSpi.engineSetKeyEntry(AndroidKeyStoreSpi.java:294)
    at java.security.KeyStore.setKeyEntry(KeyStore.java:1179)

which gets thrown here:

androidKeyStore.setKeyEntry(alias, 
        keyPair.getPrivate(),
        null,
        new Certificate[]{ generateCertificate(keyPair) });

(The certificate is generated as shown here except I added case "ECDSA": below case "EC":)

The problem seems to be the algorithm name, it's ECDSA instead of EC; when I use other means to obtain the key pair and its algorithm is EC, it works without issues.

What's going on here? Can I convert ECDSA keys to EC keys? Can I make BouncyCastle create EC keys in the first place?

For the record, this is how I create KeyPairs from byte array and passphrase: Edit: see fixed code in my answer below

1

There are 1 best solutions below

0
On

One trick is to not use Bouncy Castle for converting PCKS1 keys. On Android 10, the built-in BC provider does that part just fine. To get a key pair, call getKeyPair() on an object returned by simply JcaPEMKeyConverter() instead of JcaPEMKeyConverter().setProvider(bouncyCastleProvider).

full code:

private val bouncyCastleProvider = BouncyCastleProvider()
private val pkcs8DecryptorProvider = JceOpenSSLPKCS8DecryptorProviderBuilder()
        .setProvider(bouncyCastleProvider)
private val pemDecryptorProviderBuilder = JcePEMDecryptorProviderBuilder()
        .setProvider(bouncyCastleProvider)

private val pkcs8pemKeyConverter = JcaPEMKeyConverter().setProvider(bouncyCastleProvider)

// do NOT use BC for converting pkcs1 as for ECDSA it creates keys with algorithm "ECDSA"
// instead of "EC", and trying to put the former inside AndroidKeyStore results in
// exception “Unsupported key algorithm: ECDSA”
private val pkcs1pemKeyConverter = JcaPEMKeyConverter()


@Throws(Exception::class)   // this method throws just too many exceptions
fun makeKeyPair(keyReader: Reader, passphrase: CharArray): KeyPair {
    var obj = PEMParser(keyReader).readObject()

    // encrypted pkcs8 file, header: -----BEGIN ENCRYPTED PRIVATE KEY-----
    //   $ openssl genpkey -aes256 -pass pass:password -algorithm EC -out ecdsa.aes256.pkcs8 \
    //     -pkeyopt ec_paramgen_curve:P-384
    //   $ pkcs8 -topk8 -v2 des3 -passout pass:password -in rsa.pem -out rsa.des3.pkcs8
    if (obj is PKCS8EncryptedPrivateKeyInfo) {
        val decryptorProvider = pkcs8DecryptorProvider.build(passphrase)
        obj = /* PrivateKeyInfo */ obj.decryptPrivateKeyInfo(decryptorProvider)
    }

    // unencrypted pkcs8 file, header: -----BEGIN PRIVATE KEY-----
    //   $ openssl genpkey -algorithm RSA -out rsa.pkcs8 -pkeyopt rsa_keygen_bits:3072
    //   $ openssl genpkey -algorithm Ed25519 -out ed25519.pkcs8
    //   $ pkcs8 -topk8 -in rsa.pem -out rsa.pkcs8
    if (obj is PrivateKeyInfo) {
        val privateKey = pkcs8pemKeyConverter.getPrivateKey(obj)

        // Ed25519 can't be converted to PEM, so generate public key manually
        // the actual type here is probably BCEdDSAPrivateKey
        if (privateKey is EdDSAKey) {
            return KeyPair(genEd25519publicKey(privateKey, obj), privateKey)
        }

        // there is no direct way of extracting the public key from the private key.
        // to simplify things, we convert the key back to PEM and read it; this gets us a key pair
        obj = /* PEMKeyPair */ PEMParser(privateKey.toPem().toReader()).readObject()
    }

    // encrypted pkcs1 file, header like: -----BEGIN RSA PRIVATE KEY-----
    //                                    Proc-Type: 4,ENCRYPTED
    //                                    DEK-Info: AES-256-CBC,3B162E06B12794EA105855E7942D5A1A
    //   $ ssh-keygen -t ecdsa -m pem -N "password" -f ecdsa.aes128.pem
    //   $ openssl genrsa -aes256 -passout pass:password 4096 -out rsa.aes256.pem
    if (obj is PEMEncryptedKeyPair) {
        val decryptorProvider = pemDecryptorProviderBuilder.build(passphrase)
        obj = /* PEMKeyPair */ obj.decryptKeyPair(decryptorProvider)
    }

    // unencrypted pkcs1 file, header like: -----BEGIN RSA PRIVATE KEY-----
    //   $ ssh-keygen -t rsa -m pem -f rsa.pem
    //   $ openssl ecparam -name secp521r1 -genkey -noout -out ecdsa.pem
    if (obj is PEMKeyPair) {
        return pkcs1pemKeyConverter.getKeyPair(obj)
    }

    if (obj == null) throw IllegalArgumentException("File does not contain PEM objects")

    throw IllegalArgumentException("Don't know how to decode " + obj.javaClass.simpleName)
}


private fun PrivateKey.toPem(): String {
    val privateKeyPemObject = JcaMiscPEMGenerator(this, null).generate()
    val stringWriter = StringWriter()
    PemWriter(stringWriter).use { it.writeObject(privateKeyPemObject) }
    return stringWriter.toString()
}


private fun genEd25519publicKey(privateKey: EdDSAKey, keyInfo: PrivateKeyInfo): PublicKey {
    val privateKeyParameters = Ed25519PrivateKeyParameters(privateKey.encoded, 0)
    val publicKeyParameters = privateKeyParameters.generatePublicKey()
    val spi = SubjectPublicKeyInfo(keyInfo.privateKeyAlgorithm, publicKeyParameters.encoded)
    val factory = KeyFactory.getInstance(privateKey.algorithm, bouncyCastleProvider)
    return factory.generatePublic(X509EncodedKeySpec(spi.encoded))
}


fun ByteArray.toReader() = InputStreamReader(ByteArrayInputStream(this))
fun String.toReader() = StringReader(this)