diff --git a/app/build.gradle b/app/build.gradle index dbc2a1a..f132c6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation 'com.github.eu-digital-green-certificates:dgca-app-core-android:e4ad73eef8' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation 'org.bouncycastle:bcprov-jdk15to18:1.70' implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.okhttp3:okhttp-tls:4.9.3" diff --git a/app/src/main/java/com/lunabeestudio/domain/extension/SecretKeySpecExt.kt b/app/src/main/java/com/lunabeestudio/domain/extension/SecretKeySpecExt.kt new file mode 100644 index 0000000..ed6683f --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/domain/extension/SecretKeySpecExt.kt @@ -0,0 +1,43 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/13/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.domain.extension + +import javax.crypto.SecretKey +import javax.security.auth.DestroyFailedException + +fun SecretKey.safeDestroy() { + try { + destroy() + } catch (e: DestroyFailedException) { + // Destroy not implemented + } catch (e: NoSuchMethodError) { + // Destroy not implemented + } +} + +val SecretKey.safeIsDestroyed: Boolean? + get() = try { + isDestroyed + } catch (e: DestroyFailedException) { + // Destroy not implemented + null + } catch (e: NoSuchMethodError) { + // Destroy not implemented + null + } + +fun SecretKey.safeUse(block: (SecretKey) -> T): T { + try { + return block(this) + } finally { + safeDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lunabeestudio/framework/crypto/BouncyCastleCryptoDataSource.kt b/app/src/main/java/com/lunabeestudio/framework/crypto/BouncyCastleCryptoDataSource.kt new file mode 100644 index 0000000..261ce33 --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/framework/crypto/BouncyCastleCryptoDataSource.kt @@ -0,0 +1,122 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/20/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.framework.crypto + +import android.annotation.SuppressLint +import android.os.Build +import com.lunabeestudio.domain.extension.safeUse +import com.lunabeestudio.framework.utils.SelfDestroyCipherOutputStream +import com.lunabeestudio.robert.datasource.SharedCryptoDataSource +import com.lunabeestudio.robert.extension.use +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.jce.spec.ECParameterSpec +import java.io.ByteArrayOutputStream +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.SecureRandom +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import javax.crypto.Cipher +import javax.crypto.KeyAgreement +import javax.crypto.Mac +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class BouncyCastleCryptoDataSource : SharedCryptoDataSource { + override fun createECDHKeyPair(): KeyPair { + val ecSpec: ECParameterSpec = ECNamedCurveTable.getParameterSpec(NAMED_CURVE_SPEC) + val bouncyCastleProvider = BouncyCastleProvider() + val keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_EC, bouncyCastleProvider) + keyPairGenerator.initialize(ecSpec, SecureRandom()) + return keyPairGenerator.generateKeyPair() + } + + override fun getEncryptionKeys( + rawServerPublicKey: ByteArray, + rawLocalPrivateKey: ByteArray, + derivationDataArray: List, + ): List { + val bouncyCastleProvider = BouncyCastleProvider() + val keyFactory = KeyFactory.getInstance(ALGORITHM_EC, bouncyCastleProvider) + val serverPublicKey = keyFactory.generatePublic(X509EncodedKeySpec(rawServerPublicKey)) + val localPrivateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(rawLocalPrivateKey)) + + val keyAgreement = KeyAgreement.getInstance(ALGORITHM_ECDH) + + keyAgreement.init(localPrivateKey) + keyAgreement.doPhase(serverPublicKey, true) + + return keyAgreement.generateSecret().use { sharedSecret -> + SecretKeySpec(sharedSecret, HASH_HMACSHA256).safeUse { secretKeySpec -> + val hmac = Mac.getInstance(HASH_HMACSHA256) + hmac.init(secretKeySpec) + derivationDataArray.map { + hmac.doFinal(it) + } + } + } + } + + override fun decrypt(key: ByteArray, encryptedData: ByteArray): ByteArray { + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + val iv: ByteArray = encryptedData.copyOfRange(0, AES_GCM_IV_LENGTH) + val ivSpec = GCMParameterSpec(AES_GCM_TAG_LENGTH_IN_BITS, iv) + + return SecretKeySpec(key, ALGORITHM_AES).safeUse { secretKey -> + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) + cipher.doFinal(encryptedData, AES_GCM_IV_LENGTH, encryptedData.size - AES_GCM_IV_LENGTH) + } + } + + @SuppressLint("ObsoleteSdkInt") + override fun encrypt(key: ByteArray, clearData: ByteArray): ByteArray { + val secretKey = SecretKeySpec(key, HASH_HMACSHA256) + val bos = ByteArrayOutputStream() + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + val iv: ByteArray = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + cipher.iv + } else { + val iv = ByteArray(AES_GCM_IV_LENGTH) + SecureRandom().nextBytes(iv) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv)) + iv + } + + bos.write(iv) + + SelfDestroyCipherOutputStream(bos, cipher, secretKey).use { cos -> + clearData.inputStream().use { input -> + input.copyTo(cos, BUFFER_SIZE) + } + } + + return bos.toByteArray() + } + + companion object { + private const val HASH_HMACSHA256 = "HmacSHA256" + private const val NAMED_CURVE_SPEC = "secp256r1" + private const val ALGORITHM_EC = "EC" + private const val ALGORITHM_ECDH = "ECDH" + private const val ALGORITHM_AES = "AES" + + private const val AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding" + private const val AES_GCM_IV_LENGTH = 12 + private const val AES_GCM_TAG_LENGTH_IN_BITS = 128 + + private const val BUFFER_SIZE = 4 * 256 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lunabeestudio/framework/crypto/ServerKeyAgreementHelper.kt b/app/src/main/java/com/lunabeestudio/framework/crypto/ServerKeyAgreementHelper.kt new file mode 100644 index 0000000..aba873f --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/framework/crypto/ServerKeyAgreementHelper.kt @@ -0,0 +1,69 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2021/22/9 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.framework.crypto + +import android.util.Base64 +import com.lunabeestudio.robert.RobertConstant +import com.lunabeestudio.robert.datasource.SharedCryptoDataSource +import com.lunabeestudio.robert.extension.use + +internal class ServerKeyAgreementHelper( + private val sharedCryptoDataSource: SharedCryptoDataSource, +) { + + private var localPrivateKey: ByteArray? = null + + fun encryptRequestData( + serverPublicKey: String, + encodedDataList: List, + ): ServerKeyAgreementData { + val rawServerKey = Base64.decode(serverPublicKey, Base64.NO_WRAP) + + val keyPair = sharedCryptoDataSource.createECDHKeyPair() + + val encodedLocalPublicKey = Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP) + val serverKeyAgreementData = sharedCryptoDataSource.getEncryptionKeys( + rawServerPublicKey = rawServerKey, + rawLocalPrivateKey = keyPair.private.encoded, + derivationDataArray = listOf( + RobertConstant.CONVERSION_STRING_INPUT.toByteArray(Charsets.UTF_8), + ) + ).first().let { + localPrivateKey = it + val encryptedDataList = encodedDataList.map { encodedData -> + sharedCryptoDataSource.encrypt(it, encodedData.toByteArray(Charsets.UTF_8)) + } + val encodedEncryptedData = encryptedDataList.map { encryptedData -> + Base64.encodeToString(encryptedData, Base64.NO_WRAP) + } + ServerKeyAgreementData(encodedLocalPublicKey, encodedEncryptedData) + } + + return serverKeyAgreementData + } + + @Throws(IllegalStateException::class) + fun decryptResponse(encryptedData: String): String { + val data = Base64.decode(encryptedData, Base64.NO_WRAP) + return try { + (localPrivateKey ?: throw IllegalStateException("localPrivateKey is null. getKeyAgreementData must be call first")).use { + sharedCryptoDataSource.decrypt(it, data).toString(Charsets.UTF_8) + } + } finally { + localPrivateKey = null + } + } + + class ServerKeyAgreementData( + val encodedLocalPublicKey: String, + val encryptedData: List, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lunabeestudio/framework/utils/CryptoHelper.kt b/app/src/main/java/com/lunabeestudio/framework/utils/CryptoHelper.kt new file mode 100644 index 0000000..d8e2780 --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/framework/utils/CryptoHelper.kt @@ -0,0 +1,39 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/20/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.framework.utils + +import com.lunabeestudio.domain.extension.safeDestroy +import java.io.InputStream +import java.io.OutputStream +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.SecretKey + +class SelfDestroyCipherInputStream(inputStream: InputStream, cipher: Cipher, private val key: SecretKey) : CipherInputStream( + inputStream, + cipher +) { + override fun close() { + super.close() + key.safeDestroy() + } +} + +class SelfDestroyCipherOutputStream(outputStream: OutputStream, cipher: Cipher, private val key: SecretKey) : CipherOutputStream( + outputStream, + cipher +) { + override fun close() { + super.close() + key.safeDestroy() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lunabeestudio/robert/RobertConstant.kt b/app/src/main/java/com/lunabeestudio/robert/RobertConstant.kt new file mode 100644 index 0000000..dc6c1fa --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/robert/RobertConstant.kt @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/04/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.robert + + +object RobertConstant { + const val STATUS_WORKER_NAME: String = "RobertManager.Status.Worker" + const val EPOCH_DURATION_S: Int = 15 * 60 + const val KA_STRING_INPUT: String = "mac" + const val KEA_STRING_INPUT: String = "tuples" + const val DEFAULT_CALIBRATION_KEY: String = "DEFAULT" + const val LAST_CONTACT_DELTA_S: Long = 24 * 60 * 60 + const val CONVERSION_STRING_INPUT: String = "conversion" + + object PREFIX { + const val C1: Byte = 0b00000001 + const val C2: Byte = 0b00000010 + const val C3: Byte = 0b00000011 + const val C4: Byte = 0b00000100 + } +} diff --git a/app/src/main/java/com/lunabeestudio/robert/datasource/SharedCryptoDataSource.kt b/app/src/main/java/com/lunabeestudio/robert/datasource/SharedCryptoDataSource.kt new file mode 100644 index 0000000..208f4a5 --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/robert/datasource/SharedCryptoDataSource.kt @@ -0,0 +1,25 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/20/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.robert.datasource + +import java.security.KeyPair + +interface SharedCryptoDataSource { + fun createECDHKeyPair(): KeyPair + fun getEncryptionKeys( + rawServerPublicKey: ByteArray, + rawLocalPrivateKey: ByteArray, + derivationDataArray: List + ): List + + fun decrypt(key: ByteArray, encryptedData: ByteArray): ByteArray + fun encrypt(key: ByteArray, clearData: ByteArray): ByteArray +} \ No newline at end of file diff --git a/app/src/main/java/com/lunabeestudio/robert/extension/ByteArrayExt.kt b/app/src/main/java/com/lunabeestudio/robert/extension/ByteArrayExt.kt new file mode 100644 index 0000000..f454656 --- /dev/null +++ b/app/src/main/java/com/lunabeestudio/robert/extension/ByteArrayExt.kt @@ -0,0 +1,27 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Authors + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Created by Lunabee Studio / Date - 2020/04/05 - for the TOUS-ANTI-COVID project + */ + +package com.lunabeestudio.robert.extension + +import java.security.SecureRandom + +fun ByteArray.randomize() { + for (i in 0 until size) { + set(i, SecureRandom().nextInt().toByte()) + } +} + +fun ByteArray.use(block: (ByteArray) -> T): T { + return try { + block(this) + } finally { + this.randomize() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/communiquons/dccaggregator/Constants.kt b/app/src/main/java/org/communiquons/dccaggregator/Constants.kt index b5e13f8..799e252 100644 --- a/app/src/main/java/org/communiquons/dccaggregator/Constants.kt +++ b/app/src/main/java/org/communiquons/dccaggregator/Constants.kt @@ -14,4 +14,7 @@ const val TEST_ANTIGENIC = "LP217198-3" // TousAntiCovid resources -const val TAC_RES_BASE_URL = "https://app.tousanticovid.gouv.fr/json/version-38/" \ No newline at end of file +const val TAC_RES_BASE_URL = "https://app.tousanticovid.gouv.fr/json/version-38/" + +// Dcc light generation server +const val DCC_LIGHT_BASE_URL: String = "https://dcclight.tousanticovid.gouv.fr/" \ No newline at end of file diff --git a/app/src/main/java/org/communiquons/dccaggregator/apis/DccLightAPI.kt b/app/src/main/java/org/communiquons/dccaggregator/apis/DccLightAPI.kt new file mode 100644 index 0000000..b532ad3 --- /dev/null +++ b/app/src/main/java/org/communiquons/dccaggregator/apis/DccLightAPI.kt @@ -0,0 +1,93 @@ +package org.communiquons.dccaggregator.apis + +import com.google.gson.Gson +import com.lunabeestudio.framework.crypto.BouncyCastleCryptoDataSource +import com.lunabeestudio.framework.crypto.ServerKeyAgreementHelper +import org.communiquons.dccaggregator.DCC_LIGHT_BASE_URL +import org.communiquons.dccaggregator.data.Certificate +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +internal class ApiAggregateRQ( + val publicKey: String, + val certificates: List, +) + +internal class ApiAggregateRS( + val response: String, +) + +internal class ApiAggregateCertificate( + val certificate: String, +) + + +internal interface DccLightApi { + @POST("/api/v1/aggregate") + suspend fun aggregate( + @Body apiAggregateRQ: ApiAggregateRQ, + ): Response +} + +sealed class AggregateResult { + class Error(val error: String, val throwable: Throwable?) : AggregateResult() + class Success(val cert: Certificate) : AggregateResult() +} + +class DccLightAPIClient { + private val api by lazy { + getRetrofitService(DCC_LIGHT_BASE_URL, DccLightApi::class.java) + } + + private val serverKeyAgreementHelper by lazy { + ServerKeyAgreementHelper(BouncyCastleCryptoDataSource()) + } + + private val gson = Gson() + + suspend fun aggregateCertificates( + serverPubKey: String, + certs: List + ): AggregateResult { + // Encrypt request + val serverKeyAgreementData = try { + serverKeyAgreementHelper.encryptRequestData(serverPubKey, certs.map { c -> c.encoded }) + } catch (e: Exception) { + return AggregateResult.Error(e.message ?: "Failed to cipher request", e) + } + + val apiAggregateRQ = ApiAggregateRQ( + serverKeyAgreementData.encodedLocalPublicKey, + serverKeyAgreementData.encryptedData, + ) + + @Suppress("BlockingMethodInNonBlockingContext") + return try { + val response = api.aggregate(apiAggregateRQ) + if (response.isSuccessful) { + response.body()?.let { + val multipassJson = serverKeyAgreementHelper.decryptResponse(it.response) + val multipass = gson.fromJson( + multipassJson, + ApiAggregateCertificate::class.java + ).certificate + AggregateResult.Success(Certificate(multipass)) + } ?: AggregateResult.Error( + "Response successful (${response.code()}) but body is empty", + null + ) + + } else { + val error = response.errorBody()?.string() + if (error != null) { + AggregateResult.Error("Aggregate request failed! $error", null) + } else { + AggregateResult.Error("Request failed with status ${response.code()}", null) + } + } + } catch (e: Exception) { + return AggregateResult.Error("Request failed", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/communiquons/dccaggregator/viewmodel/CertsManagerViewModel.kt b/app/src/main/java/org/communiquons/dccaggregator/viewmodel/CertsManagerViewModel.kt index 2e05395..6296d1c 100644 --- a/app/src/main/java/org/communiquons/dccaggregator/viewmodel/CertsManagerViewModel.kt +++ b/app/src/main/java/org/communiquons/dccaggregator/viewmodel/CertsManagerViewModel.kt @@ -9,10 +9,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.communiquons.dccaggregator.apis.AggregateResult +import org.communiquons.dccaggregator.apis.DccLightAPIClient import org.communiquons.dccaggregator.apis.TousAntiCovidAPIClient import org.communiquons.dccaggregator.helper.CertificatesList class CertsManagerViewModel() : ViewModel() { + private val TAG = CertsManagerViewModel::class.java.canonicalName lateinit var certsManager: CertificatesList @@ -27,7 +30,17 @@ class CertsManagerViewModel() : ViewModel() { val config = TousAntiCovidAPIClient().getConfig() Log.d("Server Primary Key", config.activityPassGenerationServerPublicKey) - throw java.lang.Exception("TODO: send request") + // Then aggregate certificates + val res = DccLightAPIClient().aggregateCertificates( + config.activityPassGenerationServerPublicKey, + certsManager.certsList + ) + + if (res is AggregateResult.Success) { + Log.d(TAG, res.cert.encoded) + } else if (res is AggregateResult.Error) { + throw Exception(res.error) + } withContext(Dispatchers.Main) { _reqStatus.value = null