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?
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'))