Request certificates aggregation

This commit is contained in:
Pierre HUBERT 2022-02-17 10:04:31 +01:00
parent 13ae15491f
commit b2d9bd3edb
11 changed files with 466 additions and 2 deletions

View File

@ -56,6 +56,7 @@ dependencies {
implementation 'com.github.eu-digital-green-certificates:dgca-app-core-android:e4ad73eef8' implementation 'com.github.eu-digital-green-certificates:dgca-app-core-android:e4ad73eef8'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' 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.retrofit2:retrofit:2.9.0"
implementation "com.squareup.okhttp3:okhttp-tls:4.9.3" implementation "com.squareup.okhttp3:okhttp-tls:4.9.3"

View File

@ -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 <T> SecretKey.safeUse(block: (SecretKey) -> T): T {
try {
return block(this)
} finally {
safeDestroy()
}
}

View File

@ -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<ByteArray>,
): List<ByteArray> {
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
}
}

View File

@ -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<String>,
): 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<String>,
)
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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<ByteArray>
): List<ByteArray>
fun decrypt(key: ByteArray, encryptedData: ByteArray): ByteArray
fun encrypt(key: ByteArray, clearData: ByteArray): ByteArray
}

View File

@ -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 <T> ByteArray.use(block: (ByteArray) -> T): T {
return try {
block(this)
} finally {
this.randomize()
}
}

View File

@ -15,3 +15,6 @@ const val TEST_ANTIGENIC = "LP217198-3"
// TousAntiCovid resources // TousAntiCovid resources
const val TAC_RES_BASE_URL = "https://app.tousanticovid.gouv.fr/json/version-38/" 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/"

View File

@ -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<String>,
)
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<ApiAggregateRS>
}
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<Certificate>
): 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)
}
}
}

View File

@ -9,10 +9,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.apis.TousAntiCovidAPIClient
import org.communiquons.dccaggregator.helper.CertificatesList import org.communiquons.dccaggregator.helper.CertificatesList
class CertsManagerViewModel() : ViewModel() { class CertsManagerViewModel() : ViewModel() {
private val TAG = CertsManagerViewModel::class.java.canonicalName
lateinit var certsManager: CertificatesList lateinit var certsManager: CertificatesList
@ -27,7 +30,17 @@ class CertsManagerViewModel() : ViewModel() {
val config = TousAntiCovidAPIClient().getConfig() val config = TousAntiCovidAPIClient().getConfig()
Log.d("Server Primary Key", config.activityPassGenerationServerPublicKey) 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) { withContext(Dispatchers.Main) {
_reqStatus.value = null _reqStatus.value = null