Categories
Android Crypto Java Kotlin

Java EC crypto with BouncyCastle

Java crypto is like an architect building a house. Architect only knows how to design a house, but needs a construction company to actually build it. In Java, only crypto interface is defined, and a provider is required to actually implement the methods. There are different providers, but one has stood out in the test of times.

BouncyCastle

De facto open source standard for the crypto implementation is BouncyCastle(BC). It is maintained for both Java and C# and trusted by sources like Android and OkHttp.

BouncyCastle and Android

Due to Android including a cut down version of BC in the SDK, the Australian crypto provider could not at all be used as a provider in earlier phone systems. The package names are the same and would clash. To fix this problem, since Android 3, the phone’s internal copy was renamed to “com.android.org.bouncycastle”.

However, if using "BC" as the crypto provider:

Mac.getInstance("HmacSHA256", "BC")

the Android’s internal BC will be used, where “HmacSHA256” is unavailable. Running the above code will thus fail to complete.

If using BouncyCastleProvider()

Mac.getInstance("HmacSHA256", BouncyCastleProvider())

the real provider will be used, and the code will successfully run. Since Android 9 (API level 28), the BC has been removed from Android completely. This should fix the problem with duplicate Bouncy providers.

Other notes:

Currently, in Android, building with the latest BouncyCastle bcprov-jdk15on:1.68 will fail:

"Kotlin: Unresolved reference: bouncycastle"

To fix this, bcprov-jdk15to18:1.68 should be used instead.

SpongyCastle

Using projects like SpongyCastle, that port the BC classes to new namespaces, is discouraged. They haven’t been updated since 2017.

Convert to and from raw EC key bytes

Crypto keys can be stored in an array of different formats. In High-Mobility, we use both PKCS8 and raw bytes formats.

In the end, all of the formats are a representation of some raw values that are used to calculate signatures and other crypto functions. Like same factory manufactured T-shirts, but with different logos for each fashion brand.

When creating a key pair with OpenSSL, these raw values can be received:

$ openssl ecparam -genkey -out testsk.pem -name prime256v1
$ openssl ec -in testsk.pem -pubout -text
Private-Key: (256 bit)
priv:
    6c:ab:c1:a8:59:64:9d:b7:58:e9:25:bc:35:53:f1:
    4f:4e:30:65:de:b3:c6:cf:7e:f8:31:cb:14:a6:93:
    55:a3
pub:
    04:6c:e1:2c:7a:bd:ff:05:31:98:ed:e8:8a:bc:d3:
    c3:1c:ee:00:ae:59:8b:a4:6a:1b:f9:61:c9:09:fe:
    b3:b0:1e:53:1f:af:56:6b:af:9b:4b:db:14:73:4b:
    f0:ad:c3:56:18:13:bf:9f:2f:40:fc:26:ab:2b:fe:
    cf:d6:4f:02:42

Java crypto, however, doesn’t understand hex values.

Transform the public key

To create a Java Public key object, the EC spec needs to be provided and key generated with KeyFactory:

// the first 04 is the header byte
val rawPublicKey = Bytes("046ce12c7abdff053198ede88abcd3c31cee00ae598ba46a1bf961c909feb3b01e531faf566baf9b4bdb14734bf0adc3561813bf9f2f40fc26ab2bfecfd64f0242")

val params = ECNamedCurveTable.getParameterSpec("secp256r1")
val keySpec =
    ECPublicKeySpec(params.curve.decodePoint(rawPublicKey.byteArray), params)

val keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider())
val javaPublicKey = keyFactory.generatePublic(keySpec) as ECPublicKey

To convert these back to the original 65 bytes, the public key x and y coordinates can be queried from the ECPublicKey object and copied into a single byte array.

val publicPoint = javaPublicKey.w
val convertedBack = Bytes(65)
convertedBack.set(0, 0x04)
// the BigInteger can sometimes be 31 bytes. Then 0x00 needs to be prepended 
convertedBack.set(1, publicPoint.affineX.toBytes(32))
convertedBack.set(32, publicPoint.affineY.toBytes(32))

require(rawPublicKey == convertedBack)

full code

Transform the private key

Since EC private key is an integer, it’s conversion doesn’t require copying byte arrays. The hex value only needs to be converted into an integer and used in the Private Key Spec:

val rawPrivateKey = Bytes("6cabc1a859649db758e925bc3553f14f4e3065deb3c6cf7ef831cb14a69355a3").byteArray

val d = BigInteger(1, rawPrivateKey)
val curveSpec = ECParameterSpec(params.curve, params.g, params.n, params.h)
val privateKeySpec = ECPrivateKeySpec(d, curveSpec)

val keyFactory = KeyFactory.getInstance("EC")
val privateKey = keyFactory.generatePrivate(privateKeySpec) as ECPrivateKey

Transforming back means just converting the integer into a byte array:

// assure the d value is always 32 bytes long
val convertedBack = privateKey.d.toBytes(32)
require(rawPrivateKey == convertedBack)

full code

Sign and verify

EC signature is composed of raw R and S components. Fortunately, BouncyCastle provides a signing algorithm "SHA256withPLAIN-ECDSA", that can be used with raw values:

val message = Bytes("aabb")
val signer = Signature.getInstance("SHA256withPLAIN-ECDSA", BouncyCastleProvider())
signer.initSign(privateKey)
signer.update(message.byteArray)
val signature = signer.sign()

Verify can use the same signing algorithm, but Signature instance needs to be set to verifying mode:

val verifier = Signature.getInstance("SHA256withPLAIN-ECDSA", BouncyCastleProvider())
verifier.initVerify(publicKey)
verifier.update(message.byteArray)
val result = verifier.verify(signature)
require(result == true)

full code

Create a HMAC

For a pre-existing secret key and message bytes, the HMAC can be constructed using the SecretKeySpec and Mac classes

val sharedSecretKey = byteArrayOf(0, 1, 2)
val message = byteArrayOf(0, 1, 2)

val key: SecretKey = SecretKeySpec(sharedSecretKey, "HmacSHA256")
val mac: Mac = Mac.getInstance("HmacSHA256", BouncyCastleProvider())
mac.init(key)
val hmac = mac.doFinal(message)

full code

Other crypto libraries

There is the more modern Tink, which promises a cleaner and harder to misuse API. It is from Google developers and supported on multiple platforms, including Java and Android. This is a welcome change to Java’s crypto space, where currently the HMAC instance has to be initialised with a String value:

SecretKeySpec(sharedSecretKey, "HmacSHA256")

In Tink, this call becomes more object oriented:

KeysetHandle.generateNew(HmacKeyManager.hmacSha256HalfDigestTemplate());

Conclusion

Since Java doesn’t provide the implementation for the crypto functions, a separate provider needs to be used. BouncyCastle is suitable for this task. Unfortunately, in Android, it is already included in the SDK. Namespace conflicts arise and the real BC provider can be used by inputting BouncyCastleProvider() instead of "BC".

Sample code: github

hmkit-crypto-telematics: github