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 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:
the Android’s internal BC will be used, where “HmacSHA256” is unavailable. Running the above code will thus fail to complete.
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.
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.
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
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)
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)
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)
Create a HMAC
For a pre-existing secret key and message bytes, the HMAC can be constructed using the
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)
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:
In Tink, this call becomes more object oriented:
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
Sample code: github