Search code examples
androidnode.jskotlinencryptioncryptojs

Error parsing public key with Kotlin and Nodejs


I'm trying to make an encrypted communication bridge with Android and Nodejs. In both cases it can generate key-pairs with their private and public keys. Only when generating a shared secret key I get an error with the following message "Error parsing public key".

I will explain it step by step:

Step 1: Generate key pairs in Android(Kotlin)

private fun generateKeyPair(): KeyPair {
val keyPairGenerator = KeyPairGenerator.getInstance("EC")
keyPairGenerator.initialize(ECGenParameterSpec("secp256r1"))
return keyPairGenerator.generateKeyPair()
}
val pk = generateKeyPair()
val publicKey = Base64.getEncoder().encodeToString(pk.public.encoded)
//Send "publicKey" to NodeJS Server

Step 2: Generate key pairs + generate shared secret in NodeJS

const publicKey = post.publicKey; //get public key of android device

const keyPair = crypto.createECDH('prime256v1');
keyPair.generateKeys();

// Define the EC public key schema
const ECPublicKey = asn1.define('ECPublicKey', function() {
    this.seq().obj(
        this.key('algorithm').seq().obj(
            this.key('id').objid(),
            this.key('curve').objid()
        ),
        this.key('pub').bitstr()
    );
});

fun getAESKey(sharedSecret: ByteArray): ByteArray {
     val digest = MessageDigest.getInstance("SHA-512")
     return digest.digest(sharedSecret).copyOfRange(0, 32)
}

const publicKeyDer = Buffer.from(publicKey, 'base64');
const publicKeyParsed = ECPublicKey.decode(publicKeyDer, 'der');
const publicKeyBuffer = Buffer.from(publicKeyParsed.pub.data, 'base64');
const sharedSecret = keyPair.computeSecret(publicKeyBuffer);
console.log("This is the sharedSecret:",sharedSecret.toString('base64'))
const pubKeyBase = keyPair.getPublicKey().toString('base64')
//reply the value of "pubKeyBase" to android device

Step 3: Generate shared secret in Android

val publicKeyB: String = "PUBLICKEY-OF-NODEJS"
val decKey: ByteArray = Base64.getDecoder().decode(publicKeyB)

val keyFactory = KeyFactory.getInstance("EC")
val genPublicKey = keyFactory.generatePublic(X509EncodedKeySpec(decKey)) //Error: java.security.spec.InvalidKeySpecException

val sharedSecret = getSharedSecret(pk.private, genPublicKey)
val aesKey = getAESKey(sharedSecret)

In step 3 at line: keyFactory.generatePublic(X509EncodedKeySpec(decKey)) Is where I get the error:

java.security.spec.InvalidKeySpecException: com.android.org.conscrypt.OpenSSLX509CertificateFactory$ParsingException: Error parsing public key

I looked it up but it seems that I am using the wrong EncodesKeySPec? Where do I go wrong?


Solution

  • X509EncodedKeySpec() in the Kotlin code requires a DER encoded key in X.509/SPKI format. In contrast, keyPair.getPublicKey() in the NodeJS code is a public key in uncompressed format.

    The public key in uncompressed format can be converted to a PEM encoded key in X.509/SPKI format using e.g. the eckey-utils package and the generatePem() function.

    The PEM encoded key can then easily be converted to a DER encoded key by removing header, footer and line breaks and Base64 decoding the rest. Alternatively, a conversion with the crypto module using createPublicKey() and export() is possible.

    Sample code:

    const crypto = require('crypto')
    const ecKeyUtils = require('eckey-utils');
    
    // create key pair
    const keyPair = crypto.createECDH('prime256v1')
    keyPair.generateKeys()
    
    // get public key in uncompressed format
    var publicKey = keyPair.getPublicKey()
    
    // export as hex encoded uncompressed key (0x04 + <x> + <y>)
    console.log(publicKey.toString('hex'))
    
    // export as PEM encoded key in X.509/SPKI format: -----BEGIN PUBLIC KEY-----...
    var x509Pem = ecKeyUtils.generatePem({curveName: 'prime256v1', publicKey: publicKey}).publicKey // PEM encoded
    console.log(x509Pem)
    
    // export as Base64 encoded DER encoded key in X.509/SPKI format (X509EncodedKeySpec() requires the DER encoded key in X.509/SPKI format)
    var x509Der = crypto.createPublicKey(x509Pem).export({type: 'spki', format: 'der'}) // DER encoded
    var x509DerB64 = x509Der.toString('base64') // Base64 encoded DER encoded
    console.log(x509DerB64) 
    

    The import on the Kotlin side is then successful with:

    ...
    val x509DerB64 = "MFkwEw..."
    val decKey = Base64.getDecoder().decode(x509DerB64)
    val keyFactory = KeyFactory.getInstance("EC")
    val genPublicKey = keyFactory.generatePublic(X509EncodedKeySpec(decKey)) 
    ...
    

    Edit: Reverse conversion, i.e. from an X.509/SPKI key to an uncompressed key using eckey-utils:

    // get DER encoded key in X.509/SPKI format
    var x509Der = Buffer.from(x509DerB64, 'base64') // DER encoded
    
    // export as PEM encoded key in X.509/SPKI format
    var x509Pem = crypto.createPublicKey({key: x509Der, type: 'spki', format: 'der'}).export({type: 'spki', format: 'pem'}) // PEM encoded
    console.log(x509Pem)
    
    // export as uncompressed key
    const pubKey = ecKeyUtils.parsePem(x509Pem.trim()).publicKey; // uncompressed key // note the trim() to remove trailing whitespaces, which parsePem() does not allow
    console.log(publicKey.toString('hex'))