From 7aeea3adf457026b19bd595400b9202700d5f20b Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Thu, 22 Sep 2022 13:30:42 +0300 Subject: [PATCH 1/7] 1: Use DirectEncryptor with DIR and A256GCM for JWE --- .../usecases/crypto/KeyImportUseCase.kt | 29 ++++++++++--------- .../infrastructure/crypto/CryptoServer.kt | 8 ++--- gradle/libs.versions.toml | 3 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt index 17718f4..289d7fe 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt @@ -102,25 +102,26 @@ class KeyImportUseCase @Inject constructor( return KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput } - // TODO SMAR-1587: Test 2 // Encrypt a message at server with TEK using JWE format and encode as Base64 - // runTestCryptJweWithTekAtServer(serverStorage) - // - // // Handoff - // log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string ..") - // clientStorage.messageTekEncryptedJweEncodedBase64 = serverStorage.messageTekEncryptedJweEncodedBase64 - // log.d("String: ${clientStorage.messageTekEncryptedJweEncodedBase64}") - // log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string - success") + runTestCryptJweWithTekAtServer(serverStorage) + + // Handoff + log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string ..") + clientStorage.messageTekEncryptedJweEncodedBase64 = serverStorage.messageTekEncryptedJweEncodedBase64 + log.d("String: ${clientStorage.messageTekEncryptedJweEncodedBase64}") + log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string - success") // Decrypt at client with imported key - // runTestDecryptJweWithImportedKeyAtClient(clientStorage) + runTestDecryptJweWithImportedKeyAtClient(clientStorage) // Test 2: compare the input and output - // if (serverStorage.serverSecretMessageAtServer != clientStorage.decryptedServerJweMessage) { - // log.d("Test 2 decryption result \"${clientStorage.decryptedServerTextMessage}\" different " + - // "than the server string \"${clientStorage.decryptedServerJweMessage}\".") - // return KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput - // } + if (serverStorage.serverSecretMessageAtServer != clientStorage.decryptedServerJweMessage) { + log.d( + "Test 2 decryption result \"${clientStorage.decryptedServerTextMessage}\" different " + + "than the server string \"${clientStorage.decryptedServerJweMessage}\"." + ) + return KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput + } log.d("Import finished") return getKeyFinalType(clientStorage) diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt index 0aeee79..eb860b1 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt @@ -144,10 +144,10 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { override fun encryptMessageWithTekToJWE(message: String, tekAesKeyAtServer: SecretKeySpec): String { // Create the header - // (“enc”=”A128CBC-HS256”, “alg”=”dir”), - // https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/490/jwe-with-shared-key-support-for-android - val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256) - // Set the plain text + // Uses GCM for now, + // for more info see https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/490/jwe-with-shared-key-support-for-android + val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + // Set the message as payload plain text val payload = Payload(message) // Create the JWE object and encrypt it diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c4d8fd..46dc444 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,6 +89,5 @@ test-androidx-junit = 'androidx.test.ext:junit:1.1.3' test-espresso-core = 'androidx.test.espresso:espresso-core:3.4.0' # Crypto -nimbus = 'com.nimbusds:nimbus-jose-jwt:9.23' -# bouncycastle = 'org.bouncycastle:bcpkix-jdk15to18:1.68' +nimbus = 'com.nimbusds:nimbus-jose-jwt:9.25.1' bouncycastle= 'org.bouncycastle:bcprov-jdk16:1.46' From f58ef79e8cd45019e34cc14dc469c7aa08f29d59 Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Thu, 22 Sep 2022 15:55:25 +0300 Subject: [PATCH 2/7] 1: Separate key import and usage test. Do the usage both ways using JWE. --- .../domain/entities/DomainException.kt | 8 + .../domain/entities/ErrorCode.kt | 2 + .../domain/entities/KeyImportTestResult.kt | 5 +- .../entities/KeyLocalUsageTestResult.kt | 7 - .../domain/entities/KeyUsageTestResult.kt | 14 ++ .../domain/gateway/CryptoClientGateway.kt | 43 ++---- .../domain/gateway/CryptoServerGateway.kt | 2 +- .../domain/storage/ServerSecretKeyStorage.kt | 7 + .../usecases/auth/DeleteSessionUseCase.kt | 14 -- .../usecases/auth/HasValidSessionUseCase.kt | 22 --- .../domain/usecases/auth/LoginUseCase.kt | 24 --- .../domain/usecases/auth/LogoutUseCase.kt | 15 -- .../usecases/auth/SaveSessionUseCase.kt | 15 -- .../crypto/ImportedKeyLocalUsageUseCase.kt | 53 ------- .../crypto/ImportedKeyTwoWayUsageUseCase.kt | 91 ++++++++++++ .../usecases/crypto/KeyImportUseCase.kt | 138 +++--------------- .../crypto/LoadServerSecretKeyUseCase.kt | 14 ++ .../crypto/SaveServerSecretKeyUseCase.kt | 14 ++ .../crypto/WrappingKeyLocalUsageUseCase.kt | 55 ------- .../auth/HasValidSessionUseCaseTest.kt | 83 ----------- .../local/ServerSecretKeyPreferenceStorage.kt | 47 ++++++ .../infrastructure/crypto/CryptoClient.kt | 135 ++++++----------- .../infrastructure/crypto/CryptoServer.kt | 11 +- .../infrastructure/di/StorageModule.kt | 7 +- .../main/java/mobi/lab/keyimportdemo/App.kt | 4 - .../common/platform/LogoutMonitor.kt | 55 ------- .../common/rx/SchedulerProvider.kt | 7 +- .../lab/keyimportdemo/main/MainFragment.kt | 110 +++++++------- .../lab/keyimportdemo/main/MainViewModel.kt | 106 +++++++------- .../mobi/lab/keyimportdemo/main/TestStatus.kt | 75 ---------- .../main/UIKeyTeeSecurityLevel.kt | 27 ++++ .../lab/keyimportdemo/main/UiTestStatus.kt | 63 ++++++++ app/src/main/res/layout/fragment_main.xml | 22 +-- app/src/main/res/values/strings.xml | 16 +- 34 files changed, 496 insertions(+), 815 deletions(-) delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyLocalUsageTestResult.kt create mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyUsageTestResult.kt create mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/storage/ServerSecretKeyStorage.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/DeleteSessionUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LoginUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LogoutUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/SaveSessionUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyLocalUsageUseCase.kt create mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt create mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/LoadServerSecretKeyUseCase.kt create mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/SaveServerSecretKeyUseCase.kt delete mode 100644 app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/WrappingKeyLocalUsageUseCase.kt delete mode 100644 app-domain/src/test/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCaseTest.kt create mode 100644 app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/auth/local/ServerSecretKeyPreferenceStorage.kt delete mode 100644 app/src/main/java/mobi/lab/keyimportdemo/common/platform/LogoutMonitor.kt delete mode 100644 app/src/main/java/mobi/lab/keyimportdemo/main/TestStatus.kt create mode 100644 app/src/main/java/mobi/lab/keyimportdemo/main/UIKeyTeeSecurityLevel.kt create mode 100644 app/src/main/java/mobi/lab/keyimportdemo/main/UiTestStatus.kt diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/DomainException.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/DomainException.kt index ddc94c6..24d8305 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/DomainException.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/DomainException.kt @@ -31,5 +31,13 @@ class DomainException( fun noSuchImportedKeyFound(keyAlias: String): DomainException { return DomainException(ErrorCode.NO_SUCH_IMPORTED_KEY_FOUND, keyAlias) } + + fun noSuchServerKeyFound(keyAlias: String): DomainException { + return DomainException(ErrorCode.NO_SUCH_SERVER_KEY_FOUND, keyAlias) + } + + fun decryptedMessageDoesNotMatchTheOriginalMessage(message: String): DomainException { + return DomainException(ErrorCode.DECRYPTED_MESSAGE_DOES_NOT_MATCH_THE_ORIGINAL_MESSAGE, message) + } } } diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/ErrorCode.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/ErrorCode.kt index aea02dd..60e0251 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/ErrorCode.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/ErrorCode.kt @@ -6,6 +6,8 @@ enum class ErrorCode(val code: String) { LOCAL_UNAUTHORIZED("local-unauthorized"), LOCAL_NO_NETWORK("local-no-network"), NO_SUCH_IMPORTED_KEY_FOUND("no-such-imported-key-found"), + NO_SUCH_SERVER_KEY_FOUND("no-such-server-key-found"), + DECRYPTED_MESSAGE_DOES_NOT_MATCH_THE_ORIGINAL_MESSAGE("decrypted-message-does-not-match-the-original-message"), LOCAL_INVALID_CREDENTIALS("local-invalid-credentials"); companion object { diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyImportTestResult.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyImportTestResult.kt index bdace02..8cfed94 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyImportTestResult.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyImportTestResult.kt @@ -1,10 +1,7 @@ package mobi.lab.keyimportdemo.domain.entities sealed class KeyImportTestResult { - data class SuccessHardwareTeeStrongBox(val message: String) : KeyImportTestResult() - data class SuccessHardwareTeeNoStrongbox(val message: String) : KeyImportTestResult() - data class SuccessSoftwareTeeOnly(val message: String) : KeyImportTestResult() - data class SuccessTeeUnknown(val message: String) : KeyImportTestResult() + data class Success(val keyTestResult: KeyUsageTestResult) : KeyImportTestResult() object FailedKeyImportNotSupportedOnThisApiLevel : KeyImportTestResult() object FailedKeyImportNotAvailableOnThisDevice : KeyImportTestResult() object FailedTestDecryptionResultDifferentThanInput : KeyImportTestResult() diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyLocalUsageTestResult.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyLocalUsageTestResult.kt deleted file mode 100644 index 3815163..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyLocalUsageTestResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package mobi.lab.keyimportdemo.domain.entities - -sealed class KeyLocalUsageTestResult { - data class UsageSuccess(val message: String) : KeyLocalUsageTestResult() - object UsageFailedNoSuchKey : KeyLocalUsageTestResult() - data class UsageFailedGeneric(val throwable: Throwable) : KeyLocalUsageTestResult() -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyUsageTestResult.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyUsageTestResult.kt new file mode 100644 index 0000000..fe9aaa3 --- /dev/null +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/entities/KeyUsageTestResult.kt @@ -0,0 +1,14 @@ +package mobi.lab.keyimportdemo.domain.entities + +import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway + +sealed class KeyUsageTestResult { + data class UsageSuccess( + val keyLevel: CryptoClientGateway.KeyTeeSecurityLevel, + val serverToClientMessage: String, + val clientToServerMessage: String + ) : KeyUsageTestResult() + + object UsageFailedNoSuchKey : KeyUsageTestResult() + data class UsageFailedGeneric(val throwable: Throwable) : KeyUsageTestResult() +} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt index 6355974..dac2851 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt @@ -1,62 +1,37 @@ package mobi.lab.keyimportdemo.domain.gateway -import java.security.NoSuchAlgorithmException -import java.security.NoSuchProviderException import java.security.PublicKey -import java.security.spec.InvalidKeySpecException @Suppress("EmptyClassBlock") interface CryptoClientGateway { - @Throws(Exception::class) fun generateRsaKeyPairInDeviceTee(alias: String, isStrongBoxBacked: Boolean): PublicKey - - @Throws(NoSuchAlgorithmException::class, NoSuchProviderException::class, InvalidKeySpecException::class) - fun getSecretKeySecurityLevel(keyStoreKeyAlias: String): KeySecurityLevel - - @Throws(NoSuchAlgorithmException::class, NoSuchProviderException::class, InvalidKeySpecException::class) - fun getPrivateKeySecurityLevel(keyStoreKeyAlias: String): CryptoClientGateway.KeySecurityLevel - - @Throws(Exception::class) + fun getSecretKeySecurityLevel(keyStoreKeyAlias: String): KeyTeeSecurityLevel + fun getPrivateKeySecurityLevel(keyStoreKeyAlias: String): KeyTeeSecurityLevel fun encodeRsaPublicKeyAsJwk(alias: String, publicKey: PublicKey): String - - @Throws(Exception::class) fun importWrappedKeyFromServer(asn1DerEncodedWrappedKey: ByteArray, wrappingKeyAliasInKeysStore: String, wrappedKeyAlias: String) + fun decryptJWEWithImportedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String + fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String): String - @Throws(Exception::class) - fun decryptJWEWithImportedWrappedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String - - @Throws(Exception::class) - fun encryptTextWithImportedKey(keyStoreKeyAlias: String, secretMessage: String): ByteArray - - @Throws(Exception::class) - fun decryptTextWithImportedKey(keyStoreKeyAlias: String, messageTekEncryptedAtClient: ByteArray): String - - @Throws(Exception::class) - fun encryptTextWithWrappingKey(keyStoreKeyAlias: String, secretMessage: String): ByteArray - - @Throws(Exception::class) - fun decryptTextWithWrappingKey(keyStoreKeyAlias: String, messageTekEncryptedAtClient: ByteArray): String - - sealed class KeySecurityLevel { - object TeeStrongbox : KeySecurityLevel() { + sealed class KeyTeeSecurityLevel { + object TeeStrongbox : KeyTeeSecurityLevel() { override fun toString(): String { return "KeySecurityLevel.TeeStrongbox" } } - object TeeHardwareNoStrongbox : KeySecurityLevel() { + object TeeHardwareNoStrongbox : KeyTeeSecurityLevel() { override fun toString(): String { return "KeySecurityLevel.TeeHardwareNoStrongbox" } } - object TeeSoftware : KeySecurityLevel() { + object TeeSoftware : KeyTeeSecurityLevel() { override fun toString(): String { return "KeySecurityLevel.TeeSoftware" } } - object Unknown : KeySecurityLevel() { + object Unknown : KeyTeeSecurityLevel() { override fun toString(): String { return "KeySecurityLevel.Unknown" } diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt index d0c57fa..72bd475 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt @@ -16,7 +16,7 @@ interface CryptoServerGateway { fun encryptTekWithCek(tekAesWrappedKey: SecretKeySpec, wrappedKeyDescription: DERSequence, cekAesKey: SecretKeySpec): EncryptedTekWrapper fun encodeTekAndCetToAsn1Der(tekEncryptedWrapper: EncryptedTekWrapper, cekEncrypted: ByteArray, tekImportMetadata: DERSequence): ByteArray fun encryptMessageWithTekToJWE(message: String, tekAesKeyAtServer: SecretKeySpec): String - fun encryptMessageWithTek(message: String, tekAesKeyAtServer: SecretKeySpec): ByteArray + fun decryptJWEWithImportedKey(messageWrappedTekEncryptedJWE: String, tekAesKeyAtServer: SecretKeySpec): String @Suppress("ArrayInDataClass") data class EncryptedTekWrapper(val encryptedTek: ByteArray, val tag: ByteArray, val initializationVector: ByteArray) diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/storage/ServerSecretKeyStorage.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/storage/ServerSecretKeyStorage.kt new file mode 100644 index 0000000..998ffe1 --- /dev/null +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/storage/ServerSecretKeyStorage.kt @@ -0,0 +1,7 @@ +package mobi.lab.keyimportdemo.domain.storage + +interface ServerSecretKeyStorage { + fun save(keyBytes: ByteArray) + fun load(): ByteArray? + fun clear() +} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/DeleteSessionUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/DeleteSessionUseCase.kt deleted file mode 100644 index e0c8962..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/DeleteSessionUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import dagger.Reusable -import mobi.lab.keyimportdemo.domain.storage.SessionStorage -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Reusable -class DeleteSessionUseCase @Inject constructor(private val sessionStorage: SessionStorage) : UseCase() { - - fun execute() { - sessionStorage.clear() - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCase.kt deleted file mode 100644 index a5368e9..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import dagger.Reusable -import io.reactivex.rxjava3.core.Completable -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode -import mobi.lab.keyimportdemo.domain.storage.SessionStorage -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Reusable -class HasValidSessionUseCase @Inject constructor(private val sessionStorage: SessionStorage) : UseCase() { - - fun execute(): Completable { - return Completable.fromSupplier { - val session = sessionStorage.load() - if (session == null || !session.isValid()) { - throw DomainException(ErrorCode.LOCAL_UNAUTHORIZED) - } - } - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LoginUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LoginUseCase.kt deleted file mode 100644 index a16fbe5..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LoginUseCase.kt +++ /dev/null @@ -1,24 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import dagger.Reusable -import io.reactivex.rxjava3.core.Single -import mobi.lab.keyimportdemo.app.common.isStringEmpty -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode -import mobi.lab.keyimportdemo.domain.entities.Session -import mobi.lab.keyimportdemo.domain.gateway.AuthGateway -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Reusable -class LoginUseCase @Inject constructor( - private val gw: AuthGateway, - private val saveSessionUseCase: SaveSessionUseCase -) : UseCase() { - fun execute(email: String, password: String): Single { - if (isStringEmpty(email) || isStringEmpty(password)) { - return Single.error(DomainException(ErrorCode.LOCAL_INVALID_CREDENTIALS)) - } - return gw.login("eve.holt@reqres.in", "cityslicka").doOnSuccess { saveSessionUseCase.execute(it) } - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LogoutUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LogoutUseCase.kt deleted file mode 100644 index dbcf095..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/LogoutUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import dagger.Reusable -import io.reactivex.rxjava3.core.Completable -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Reusable -class LogoutUseCase @Inject constructor( - private val deleteSessionUseCase: DeleteSessionUseCase -) : UseCase() { - fun execute(): Completable { - return Completable.fromCallable { deleteSessionUseCase.execute() } - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/SaveSessionUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/SaveSessionUseCase.kt deleted file mode 100644 index d01d92a..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/auth/SaveSessionUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import dagger.Reusable -import mobi.lab.keyimportdemo.domain.entities.Session -import mobi.lab.keyimportdemo.domain.storage.SessionStorage -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Reusable -class SaveSessionUseCase @Inject constructor(private val sessionStorage: SessionStorage) : UseCase() { - - fun execute(session: Session) { - sessionStorage.save(session) - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyLocalUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyLocalUsageUseCase.kt deleted file mode 100644 index 6ec101e..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyLocalUsageUseCase.kt +++ /dev/null @@ -1,53 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.crypto - -import dagger.Reusable -import io.reactivex.rxjava3.core.Single -import mobi.lab.keyimportdemo.domain.DomainConstants -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode -import mobi.lab.keyimportdemo.domain.entities.KeyLocalUsageTestResult -import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway -import mobi.lab.keyimportdemo.domain.gateway.LoggerGateway -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Suppress("UnusedPrivateMember") -@Reusable -class ImportedKeyLocalUsageUseCase @Inject constructor( - private val client: CryptoClientGateway, - private val log: LoggerGateway, -) : UseCase() { - fun execute(inputMessage: String): Single { - return Single.fromCallable { runTest(inputMessage) } - } - - private fun runTest(secretMessage: String): KeyLocalUsageTestResult { - log.d("Local usage test started") - - // Test 1 - // Use the local TEK to encrypt and decrypt a message - return try { - val result = runTestCryptAndEncryptTextWithTek(secretMessage) - log.d("Local usage test finished") - KeyLocalUsageTestResult.UsageSuccess(result) - } catch (e: DomainException) { - log.e(e, "runTest") - when (e.code) { - ErrorCode.NO_SUCH_IMPORTED_KEY_FOUND -> { - KeyLocalUsageTestResult.UsageFailedNoSuchKey - } - else -> { - KeyLocalUsageTestResult.UsageFailedGeneric(e) - } - } - } catch (t: Throwable) { - log.e(t, "runTest") - KeyLocalUsageTestResult.UsageFailedGeneric(t) - } - } - - private fun runTestCryptAndEncryptTextWithTek(secretMessage: String): String { - val encryptedSecretMessage = client.encryptTextWithImportedKey(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, secretMessage) - return client.decryptTextWithImportedKey(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, encryptedSecretMessage) - } -} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt new file mode 100644 index 0000000..b14a5bb --- /dev/null +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt @@ -0,0 +1,91 @@ +package mobi.lab.keyimportdemo.domain.usecases.crypto + +import dagger.Reusable +import io.reactivex.rxjava3.core.Single +import mobi.lab.keyimportdemo.domain.DomainConstants +import mobi.lab.keyimportdemo.domain.entities.DomainException +import mobi.lab.keyimportdemo.domain.entities.ErrorCode +import mobi.lab.keyimportdemo.domain.entities.KeyUsageTestResult +import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway +import mobi.lab.keyimportdemo.domain.gateway.CryptoServerGateway +import mobi.lab.keyimportdemo.domain.gateway.LoggerGateway +import mobi.lab.keyimportdemo.domain.usecases.UseCase +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject + +@Suppress("UnusedPrivateMember") +@Reusable +class ImportedKeyTwoWayUsageUseCase @Inject constructor( + private val client: CryptoClientGateway, + private val server: CryptoServerGateway, + private val loadServerSecretKeyUseCase: LoadServerSecretKeyUseCase, + private val log: LoggerGateway, +) : UseCase() { + fun execute(serverToClientSecretMessage: String, clientToServerSecretMessage: String): Single { + return Single.fromCallable { runTest(serverToClientSecretMessage, clientToServerSecretMessage) } + } + + private fun runTest(serverToClientSecretMessage: String, clientToServerSecretMessage: String): KeyUsageTestResult { + log.d("Local usage test started") + + return try { + log.d("Testing server -> client message flow ..") + val serverToClientSecretMessageResult = runTestServerEncryptCryptAndClientDecryptJweWithTek(serverToClientSecretMessage) + log.d("Testing server -> client message flow test finished") + + log.d("Testing client -> server message flow ..") + val clientToServerSecretMessageResult = runTestClientEncryptCryptAndServerDecryptJweWithTek(clientToServerSecretMessage) + log.d("Testing client -> server message flow test finished") + + KeyUsageTestResult.UsageSuccess( + client.getPrivateKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS), + serverToClientSecretMessageResult, + clientToServerSecretMessageResult + ) + } catch (e: DomainException) { + log.e(e, "runTest") + when (e.code) { + ErrorCode.NO_SUCH_IMPORTED_KEY_FOUND, + ErrorCode.NO_SUCH_SERVER_KEY_FOUND -> { + KeyUsageTestResult.UsageFailedNoSuchKey + } + else -> { + KeyUsageTestResult.UsageFailedGeneric(e) + } + } + } catch (t: Throwable) { + log.e(t, "runTest") + KeyUsageTestResult.UsageFailedGeneric(t) + } + } + + private fun runTestServerEncryptCryptAndClientDecryptJweWithTek(secretMessage: String): String { + val keyBytes = loadServerSecretKeyUseCase.execute() + if (keyBytes == null) { + throw DomainException.noSuchServerKeyFound(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS) + } + val keySpec = SecretKeySpec(keyBytes, "AES") + val encryptedSecretMessage = server.encryptMessageWithTekToJWE(secretMessage, keySpec) + val resultDecryptedMessage = client.decryptJWEWithImportedKey(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, encryptedSecretMessage) + assertDecryptedMessageEqualsOriginalMessage(secretMessage, resultDecryptedMessage) + return resultDecryptedMessage + } + + private fun runTestClientEncryptCryptAndServerDecryptJweWithTek(secretMessage: String): String { + val keyBytes = loadServerSecretKeyUseCase.execute() + if (keyBytes == null) { + throw DomainException.noSuchServerKeyFound(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS) + } + val keySpec = SecretKeySpec(keyBytes, "AES") + val encryptedSecretMessage = client.encryptMessageWithTekToJWE(secretMessage, DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS) + val resultDecryptedMessage = server.decryptJWEWithImportedKey(encryptedSecretMessage, keySpec) + assertDecryptedMessageEqualsOriginalMessage(secretMessage, resultDecryptedMessage) + return resultDecryptedMessage + } + + private fun assertDecryptedMessageEqualsOriginalMessage(originalMessage: String, decryptedMessage: String) { + if (originalMessage != decryptedMessage) { + throw DomainException.decryptedMessageDoesNotMatchTheOriginalMessage("Original: $originalMessage\nDecrypted: $decryptedMessage") + } + } +} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt index 289d7fe..6095c58 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt @@ -5,9 +5,9 @@ import android.security.keystore.SecureKeyImportUnavailableException import androidx.annotation.RequiresApi import dagger.Reusable import io.reactivex.rxjava3.core.Single -import mobi.lab.keyimportdemo.app.common.exhaustive import mobi.lab.keyimportdemo.domain.DomainConstants import mobi.lab.keyimportdemo.domain.entities.KeyImportTestResult +import mobi.lab.keyimportdemo.domain.entities.KeyUsageTestResult import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway import mobi.lab.keyimportdemo.domain.gateway.CryptoServerGateway import mobi.lab.keyimportdemo.domain.gateway.LoggerGateway @@ -24,9 +24,11 @@ class KeyImportUseCase @Inject constructor( private val client: CryptoClientGateway, private val server: CryptoServerGateway, private val log: LoggerGateway, + private val saveServerSecretKeyUseCase: SaveServerSecretKeyUseCase, + private val importedKeyTwoWayUsageUseCase: ImportedKeyTwoWayUsageUseCase ) : UseCase() { - fun execute(inputMessage: String): Single { - return Single.fromCallable { runTest(inputMessage) } + fun execute(serverSecretMessage: String, clientSecretMessage: String): Single { + return Single.fromCallable { runTest(serverSecretMessage, clientSecretMessage) } } /** @@ -40,7 +42,7 @@ class KeyImportUseCase @Inject constructor( * Finally, the key exchange result is tested by the server encrypting a message and the client decrypting it. */ @Suppress("LongMethod", "ReturnCount") - private fun runTest(serverSecretMessageAtServer: String): KeyImportTestResult { + private fun runTest(serverSecretMessage: String, clientSecretMessage: String): KeyImportTestResult { log.d("Import started") // Storage accessible for "client". @@ -48,7 +50,7 @@ class KeyImportUseCase @Inject constructor( // Storage accessible for "server" // In real word would be in the server instance. - val serverStorage = Storage.ServerSide(serverSecretMessageAtServer = serverSecretMessageAtServer) + val serverStorage = Storage.ServerSide() // Client: Client key init phase in Android TEE runTestClientKeyInitPhase(clientStorage) @@ -80,84 +82,11 @@ class KeyImportUseCase @Inject constructor( return KeyImportTestResult.FailedKeyImportNotAvailableOnThisDevice } - // Test 1 - // Test 1: Encrypt a message at server with TEK and return it as Base64 with the iv appended at the front (first 16 byes) of the cryptogram - runTestCryptTextWithTekAtServer(serverStorage) - - // Test 1: Handoff client -> server - log.d("HANDOFF: Server outputs the TEK encrypted message as Base64 encoded string ..") - clientStorage.messageTekEncryptedEncodedBase64 = serverStorage.messageTekEncryptedEncodedBase64 - log.d("String: ${clientStorage.messageTekEncryptedEncodedBase64}") - log.d("HANDOFF: Server outputs the TEK encrypted message as Base64 encoded string - success") - - // Test 1: Decrypt at client with imported key - runTestDecryptTextWithImportedKeyAtClient(clientStorage) - - // Test 1: compare the input and output - if (serverStorage.serverSecretMessageAtServer != clientStorage.decryptedServerTextMessage) { - log.d( - "Test 1 decryption result \"${clientStorage.decryptedServerTextMessage}\" different " + - "than the server string \"${clientStorage.decryptedServerTextMessage}\"." - ) - return KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput - } - - // Encrypt a message at server with TEK using JWE format and encode as Base64 - runTestCryptJweWithTekAtServer(serverStorage) - - // Handoff - log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string ..") - clientStorage.messageTekEncryptedJweEncodedBase64 = serverStorage.messageTekEncryptedJweEncodedBase64 - log.d("String: ${clientStorage.messageTekEncryptedJweEncodedBase64}") - log.d("HANDOFF: Server outputs the TEK JWE encrypted message as Base64 encoded string - success") - - // Decrypt at client with imported key - runTestDecryptJweWithImportedKeyAtClient(clientStorage) - - // Test 2: compare the input and output - if (serverStorage.serverSecretMessageAtServer != clientStorage.decryptedServerJweMessage) { - log.d( - "Test 2 decryption result \"${clientStorage.decryptedServerTextMessage}\" different " + - "than the server string \"${clientStorage.decryptedServerJweMessage}\"." - ) - return KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput - } + // Test messaging between the client and server both ways using the imported AES TEK + val keyTestResult = importedKeyTwoWayUsageUseCase.execute(serverSecretMessage, clientSecretMessage).blockingGet() log.d("Import finished") - return getKeyFinalType(clientStorage) - } - - private fun runTestCryptJweWithTekAtServer(serverStorage: Storage.ServerSide) { - // Server key generation phase 9. Server uses TEK key to create JWE message with payload of “Hello world” - // and outputs it as Base64 encoded string. - log.d("SERVER: Encrypting the message with TEK to JWE ..") - val messageTekEncryptedJWEAtServer = - server.encryptMessageWithTekToJWE(serverStorage.serverSecretMessageAtServer, serverStorage.tekAesKeyAtServer) - log.d("SERVER: JWE: $messageTekEncryptedJWEAtServer") - log.d("SERVER: Encrypting the message with TEK to JWE - success") - - // Encode to Base64 - serverStorage.messageTekEncryptedJweEncodedBase64 = - String( - Base64.encode(messageTekEncryptedJWEAtServer.toByteArray(Charset.forName(BASE64_ENCODING_CHARSET_NAME))), - Charset.forName(BASE64_ENCODING_CHARSET_NAME) - ) - } - - private fun runTestDecryptJweWithImportedKeyAtClient(clientStorage: Storage.ClientSide) { - // Android key import phase 2. Android app loads the server encrypted JWE message. - log.d("CLIENT: Decode the server encrypted text message from Base64 ..") - val messageTekEncryptedJweString = String( - Base64.decode(clientStorage.messageTekEncryptedJweEncodedBase64), Charset.forName(BASE64_ENCODING_CHARSET_NAME) - ) - log.d("CLIENT: JWE: $messageTekEncryptedJweString") - log.d("CLIENT: Decode the server encrypted text message from Base64 - success") - - // Android key import phase 5. Android app uses the StrongBox protected key to decrypt the JWE message from server and outputs the payload. - log.d("CLIENT: Decrypt the Server's JWE message with the imported key ..") - clientStorage.decryptedServerJweMessage = - client.decryptJWEWithImportedWrappedKey(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, messageTekEncryptedJweString) - log.d("CLIENT: Decrypt the Server's JWE message with the imported key - SKIPPED") + return getKeyFinalType(keyTestResult) } private fun runTestClientKeyInitPhase(clientStorage: Storage.ClientSide) { @@ -204,6 +133,9 @@ class KeyImportUseCase @Inject constructor( serverStorage.tekAesKeyAtServer = server.generateAesTek(TEK_KEY_SIZE_BYTES) log.d("SERVER: Generating AES TEK for Android import - success") + // Save the key locally for additional tests (the other button in the UI) + saveServerSecretKeyUseCase.execute(serverStorage.tekAesKeyAtServer.encoded) + // Server key generation phase 3. Server generates TEK key metadata, which will be used by StrongBox to import the TEK key log.d("SERVER: Generating TEK import metadata ..") val tekImportMetadata = server.generateTekImportMetadata(TEK_KEY_SIZE_BYTES, KEY_MASTER_ALGORITHM_AES) @@ -252,43 +184,13 @@ class KeyImportUseCase @Inject constructor( // Android key import phase 4. Android app imports the wrapped key log.d("CLIENT: Importing the server wrapped key to device TEE keystore ..") client.importWrappedKeyFromServer( - asn1DerEncodedTekAndCekAtClient, - DomainConstants.DEVICE_TEE_WRAPPING_KEY_ALIAS, - DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS + asn1DerEncodedTekAndCekAtClient, DomainConstants.DEVICE_TEE_WRAPPING_KEY_ALIAS, DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS ) log.d("CLIENT: Importing the server wrapped key to device TEE keystore - success") } - private fun runTestCryptTextWithTekAtServer(serverStorage: Storage.ServerSide) { - log.d("SERVER: Encrypting the message with TEK ..") - val messageTekEncryptedAtServer = server.encryptMessageWithTek(serverStorage.serverSecretMessageAtServer, serverStorage.tekAesKeyAtServer) - log.d("SERVER: Encrypting the message with TEK - success") - - // Encode to base64 - serverStorage.messageTekEncryptedEncodedBase64 = String( - Base64.encode(messageTekEncryptedAtServer), Charset.forName(BASE64_ENCODING_CHARSET_NAME) - ) - } - - private fun runTestDecryptTextWithImportedKeyAtClient(clientStorage: Storage.ClientSide) { - log.d("CLIENT: Decode the server encrypted text message from Base64 ..") - val messageTekEncryptedAtClient = Base64.decode(clientStorage.messageTekEncryptedEncodedBase64) - log.d("CLIENT: Decode the server encrypted text message from Base64 - success") - - log.d("CLIENT: Decrypt the Server's text message with the imported key ..") - clientStorage.decryptedServerTextMessage = - client.decryptTextWithImportedKey(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, messageTekEncryptedAtClient) - log.d("CLIENT: Decrypt the Server's text message with the imported key - success") - } - - private fun getKeyFinalType(clientStorage: Storage.ClientSide): KeyImportTestResult { - val text = "Test 1 message: ${clientStorage.decryptedServerTextMessage}" - return when (client.getSecretKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS)) { - CryptoClientGateway.KeySecurityLevel.TeeStrongbox -> KeyImportTestResult.SuccessHardwareTeeStrongBox(text) - CryptoClientGateway.KeySecurityLevel.TeeHardwareNoStrongbox -> KeyImportTestResult.SuccessHardwareTeeNoStrongbox(text) - CryptoClientGateway.KeySecurityLevel.TeeSoftware -> KeyImportTestResult.SuccessSoftwareTeeOnly(text) - CryptoClientGateway.KeySecurityLevel.Unknown -> KeyImportTestResult.SuccessTeeUnknown(text) - }.exhaustive + private fun getKeyFinalType(keyTestResult: KeyUsageTestResult): KeyImportTestResult { + return KeyImportTestResult.Success(keyTestResult) } sealed class Storage { @@ -297,18 +199,12 @@ class KeyImportUseCase @Inject constructor( lateinit var wrappingRsaKeyPairAsJwk: String var wasGeneratedInStrongbox: Boolean = false lateinit var asn1DerBase64EncodedTekAndCekAtServer: String - lateinit var messageTekEncryptedEncodedBase64: String - lateinit var messageTekEncryptedJweEncodedBase64: String - lateinit var decryptedServerTextMessage: String - lateinit var decryptedServerJweMessage: String } - class ServerSide(val serverSecretMessageAtServer: String) : Storage() { + class ServerSide : Storage() { lateinit var asn1DerBase64EncodedTekAndCekAtServer: String lateinit var tekAesKeyAtServer: SecretKeySpec lateinit var wrappingRsaKeyPairAsJwk: String - lateinit var messageTekEncryptedEncodedBase64: String - lateinit var messageTekEncryptedJweEncodedBase64: String } } diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/LoadServerSecretKeyUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/LoadServerSecretKeyUseCase.kt new file mode 100644 index 0000000..f420f2b --- /dev/null +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/LoadServerSecretKeyUseCase.kt @@ -0,0 +1,14 @@ +package mobi.lab.keyimportdemo.domain.usecases.crypto + +import dagger.Reusable +import mobi.lab.keyimportdemo.domain.storage.ServerSecretKeyStorage +import mobi.lab.keyimportdemo.domain.usecases.UseCase +import javax.inject.Inject + +@Reusable +class LoadServerSecretKeyUseCase @Inject constructor(private val serverSecretKeyStorage: ServerSecretKeyStorage) : UseCase() { + + fun execute(): ByteArray? { + return serverSecretKeyStorage.load() + } +} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/SaveServerSecretKeyUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/SaveServerSecretKeyUseCase.kt new file mode 100644 index 0000000..ec2e670 --- /dev/null +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/SaveServerSecretKeyUseCase.kt @@ -0,0 +1,14 @@ +package mobi.lab.keyimportdemo.domain.usecases.crypto + +import dagger.Reusable +import mobi.lab.keyimportdemo.domain.storage.ServerSecretKeyStorage +import mobi.lab.keyimportdemo.domain.usecases.UseCase +import javax.inject.Inject + +@Reusable +class SaveServerSecretKeyUseCase @Inject constructor(private val serverSecretKeyStorage: ServerSecretKeyStorage) : UseCase() { + + fun execute(keyBytes: ByteArray) { + serverSecretKeyStorage.save(keyBytes) + } +} diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/WrappingKeyLocalUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/WrappingKeyLocalUsageUseCase.kt deleted file mode 100644 index 95de44b..0000000 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/WrappingKeyLocalUsageUseCase.kt +++ /dev/null @@ -1,55 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.crypto - -import dagger.Reusable -import io.reactivex.rxjava3.core.Single -import mobi.lab.keyimportdemo.domain.DomainConstants -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode -import mobi.lab.keyimportdemo.domain.entities.KeyLocalUsageTestResult -import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway -import mobi.lab.keyimportdemo.domain.gateway.LoggerGateway -import mobi.lab.keyimportdemo.domain.usecases.UseCase -import javax.inject.Inject - -@Suppress("UnusedPrivateMember") -@Reusable -class WrappingKeyLocalUsageUseCase @Inject constructor( - private val client: CryptoClientGateway, - private val log: LoggerGateway, -) : UseCase() { - fun execute(inputMessage: String): Single { - return Single.fromCallable { runTest(inputMessage) } - } - - private fun runTest(secretMessage: String): KeyLocalUsageTestResult { - log.d("Local usage test started") - - // Test 1 - // Use the local RSA key to encrypt and decrypt a message - return try { - val result = runTestCryptAndEncryptTextWithWrappingKey(secretMessage) - log.d("Local usage test finished") - KeyLocalUsageTestResult.UsageSuccess(result) - } catch (e: DomainException) { - log.e(e, "runTest") - when (e.code) { - ErrorCode.NO_SUCH_IMPORTED_KEY_FOUND -> { - KeyLocalUsageTestResult.UsageFailedNoSuchKey - } - else -> { - KeyLocalUsageTestResult.UsageFailedGeneric(e) - } - } - } catch (t: Throwable) { - log.e(t, "runTest") - KeyLocalUsageTestResult.UsageFailedGeneric(t) - } - } - - private fun runTestCryptAndEncryptTextWithWrappingKey(secretMessage: String): String { - val encryptedSecretMessage = client.encryptTextWithWrappingKey(DomainConstants.DEVICE_TEE_WRAPPING_KEY_ALIAS, secretMessage) - val availableHardware = client.getPrivateKeySecurityLevel(DomainConstants.DEVICE_TEE_WRAPPING_KEY_ALIAS) - log.d("runTestCryptAndEncryptTextWithWrappingKey: Key in $availableHardware") - return client.decryptTextWithWrappingKey(DomainConstants.DEVICE_TEE_WRAPPING_KEY_ALIAS, encryptedSecretMessage) - } -} diff --git a/app-domain/src/test/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCaseTest.kt b/app-domain/src/test/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCaseTest.kt deleted file mode 100644 index a3106ff..0000000 --- a/app-domain/src/test/java/mobi/lab/keyimportdemo/domain/usecases/auth/HasValidSessionUseCaseTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package mobi.lab.keyimportdemo.domain.usecases.auth - -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import io.reactivex.rxjava3.core.Completable -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode -import mobi.lab.keyimportdemo.domain.entities.Session -import mobi.lab.keyimportdemo.domain.storage.SessionStorage -import org.junit.Before -import org.junit.Test -import org.mockito.MockitoAnnotations -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class HasValidSessionUseCaseTest { - - private lateinit var useCase: HasValidSessionUseCase - - private var storage: SessionStorage = mock() - - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - whenever(storage.load()).then { Session("test") } - useCase = HasValidSessionUseCase(storage) - } - - @Test - fun validSession_NULL() { - whenever(storage.load()).then { null } - val error = useCase.execute().blockingGet() - verifyUnauthorizedException(error) - } - - @Test - fun validSession_TOKEN_NULL() { - whenever(storage.load()).then { Session(null) } - val error = useCase.execute().blockingGet() - verifyUnauthorizedException(error) - } - - @Test - fun validSession_TOKEN_EMPTY_STRING() { - whenever(storage.load()).then { Session("") } - val error = useCase.execute().blockingGet() - verifyUnauthorizedException(error) - } - - @Test - fun validSession_TOKEN_EMPTY_SPACE_STRING() { - whenever(storage.load()).then { Session(" ") } - val error = useCase.execute().blockingGet() - // If there's a value, we don't trim it - assertNull(error) - } - - @Test - fun validSession_SUCCESS() { - whenever(storage.load()).then { Session("token") } - val error = useCase.execute().blockingGet() - assertNull(error) - } - - // blockingGet was removed in RxJava3 - private fun Completable.blockingGet(): Throwable? { - try { - this.blockingAwait() - return null - } catch (error: Throwable) { - // blockingAwait wraps exceptions with a RuntimeException - return error.cause - } - } - - private fun verifyUnauthorizedException(error: Throwable?) { - assertNotNull(error) - assertTrue(error is DomainException) - assertEquals(error.code, ErrorCode.LOCAL_UNAUTHORIZED) - } -} diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/auth/local/ServerSecretKeyPreferenceStorage.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/auth/local/ServerSecretKeyPreferenceStorage.kt new file mode 100644 index 0000000..6716fc5 --- /dev/null +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/auth/local/ServerSecretKeyPreferenceStorage.kt @@ -0,0 +1,47 @@ +package mobi.lab.keyimportdemo.infrastructure.auth.local + +import mobi.lab.keyimportdemo.domain.storage.ServerSecretKeyStorage +import mobi.lab.keyimportdemo.infrastructure.common.local.SharedPreferenceStorage +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class ServerSecretKeyPreferenceStorage @Inject constructor( + private val sharedPrefs: SharedPreferenceStorage, +) : ServerSecretKeyStorage { + + override fun save(keyBytes: ByteArray) { + try { + saveToPreferences(keyBytes) + } catch (error: Exception) { + Timber.w(error, "save") + } + } + + override fun load(): ByteArray? { + return try { + Timber.d("load STORAGE this=$this") + loadFromPreferences() + } catch (error: Exception) { + Timber.w(error, "load") + null + } + } + + private fun loadFromPreferences(): ByteArray? { + return sharedPrefs.getObject(KEY) + } + + private fun saveToPreferences(keyBytes: ByteArray?) { + sharedPrefs.putObject(KEY, keyBytes) + } + + override fun clear() { + sharedPrefs.putObject(KEY, null) + } + + companion object { + private const val KEY = "KEY_SERVER_KEY_BYTES" + } +} diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt index d7ac360..9bef882 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt @@ -6,13 +6,16 @@ import android.security.keystore.KeyInfo import android.security.keystore.KeyProperties import android.security.keystore.WrappedKeyEntry import androidx.annotation.RequiresApi +import com.nimbusds.jose.EncryptionMethod +import com.nimbusds.jose.JWEAlgorithm +import com.nimbusds.jose.JWEHeader import com.nimbusds.jose.JWEObject +import com.nimbusds.jose.Payload import com.nimbusds.jose.crypto.DirectDecrypter +import com.nimbusds.jose.crypto.DirectEncrypter import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.RSAKey -import mobi.lab.keyimportdemo.domain.entities.DomainException import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway -import java.security.Key import java.security.KeyFactory import java.security.KeyPairGenerator import java.security.KeyStore @@ -23,13 +26,8 @@ import java.security.PublicKey import java.security.interfaces.RSAPublicKey import java.security.spec.AlgorithmParameterSpec import java.security.spec.InvalidKeySpecException -import java.security.spec.MGF1ParameterSpec -import javax.crypto.Cipher import javax.crypto.SecretKey import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.OAEPParameterSpec -import javax.crypto.spec.PSource import javax.crypto.spec.SecretKeySpec import javax.inject.Inject import kotlin.random.Random @@ -57,7 +55,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { } @Throws(NoSuchAlgorithmException::class, NoSuchProviderException::class, InvalidKeySpecException::class) - override fun getSecretKeySecurityLevel(keyStoreKeyAlias: String): CryptoClientGateway.KeySecurityLevel { + override fun getSecretKeySecurityLevel(keyStoreKeyAlias: String): CryptoClientGateway.KeyTeeSecurityLevel { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val key: SecretKey = keyStore.getKey(keyStoreKeyAlias, null) as SecretKey @@ -69,7 +67,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { } @Throws(NoSuchAlgorithmException::class, NoSuchProviderException::class, InvalidKeySpecException::class) - override fun getPrivateKeySecurityLevel(keyStoreKeyAlias: String): CryptoClientGateway.KeySecurityLevel { + override fun getPrivateKeySecurityLevel(keyStoreKeyAlias: String): CryptoClientGateway.KeyTeeSecurityLevel { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val privateKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.PrivateKeyEntry @@ -81,60 +79,60 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { } @RequiresApi(Build.VERSION_CODES.S) - private fun getSecretKeySecurityLevelFromApi(key: SecretKey): CryptoClientGateway.KeySecurityLevel { + private fun getSecretKeySecurityLevelFromApi(key: SecretKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: SecretKeyFactory = SecretKeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) return when ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).securityLevel) { KeyProperties.SECURITY_LEVEL_STRONGBOX -> { - CryptoClientGateway.KeySecurityLevel.TeeStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeStrongbox } KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT, KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> { - CryptoClientGateway.KeySecurityLevel.TeeHardwareNoStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } KeyProperties.SECURITY_LEVEL_SOFTWARE -> { - CryptoClientGateway.KeySecurityLevel.TeeSoftware + CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware } else -> { - CryptoClientGateway.KeySecurityLevel.Unknown + CryptoClientGateway.KeyTeeSecurityLevel.Unknown } } } @RequiresApi(Build.VERSION_CODES.S) - private fun getPrivateKeySecurityLevelFromApi(key: PrivateKey): CryptoClientGateway.KeySecurityLevel { + private fun getPrivateKeySecurityLevelFromApi(key: PrivateKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: KeyFactory = KeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) return when ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).securityLevel) { KeyProperties.SECURITY_LEVEL_STRONGBOX -> { - CryptoClientGateway.KeySecurityLevel.TeeStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeStrongbox } KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT, KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> { - CryptoClientGateway.KeySecurityLevel.TeeHardwareNoStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } KeyProperties.SECURITY_LEVEL_SOFTWARE -> { - CryptoClientGateway.KeySecurityLevel.TeeSoftware + CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware } else -> { - CryptoClientGateway.KeySecurityLevel.Unknown + CryptoClientGateway.KeyTeeSecurityLevel.Unknown } } } - private fun isSecureKeyInsideSecureHardwareCompat(key: SecretKey): CryptoClientGateway.KeySecurityLevel { + private fun isSecureKeyInsideSecureHardwareCompat(key: SecretKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: SecretKeyFactory = SecretKeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) @Suppress("DEPRECATION") return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { - CryptoClientGateway.KeySecurityLevel.TeeHardwareNoStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } else { - CryptoClientGateway.KeySecurityLevel.TeeSoftware + CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware } } - private fun isPrivateKeyInsideSecureHardwareCompat(key: PrivateKey): CryptoClientGateway.KeySecurityLevel { + private fun isPrivateKeyInsideSecureHardwareCompat(key: PrivateKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: KeyFactory = KeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) @Suppress("DEPRECATION") return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { - CryptoClientGateway.KeySecurityLevel.TeeHardwareNoStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } else { - CryptoClientGateway.KeySecurityLevel.TeeSoftware + CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware } } @@ -157,72 +155,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { } @Suppress("MagicNumber") - override fun decryptTextWithImportedKey(keyStoreKeyAlias: String, messageTekEncryptedAtClient: ByteArray): String { - val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null, null) - val key: SecretKey = keyStore.getKey(keyStoreKeyAlias, null) as SecretKey - - val c: Cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") - // First 16 byte are iv - val ivPart: ByteArray = messageTekEncryptedAtClient.copyOfRange(0, 16) - val messagePart: ByteArray = messageTekEncryptedAtClient.copyOfRange(16, messageTekEncryptedAtClient.size) - val ivParamSpec = IvParameterSpec(ivPart) - c.init(Cipher.DECRYPT_MODE, key, ivParamSpec) - return String(c.doFinal(messagePart)) - } - - override fun encryptTextWithWrappingKey(keyStoreKeyAlias: String, secretMessage: String): ByteArray { - val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null, null) - - val privateKeyEntry: KeyStore.PrivateKeyEntry? = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.PrivateKeyEntry? - if (privateKeyEntry == null) { - throw DomainException.noSuchImportedKeyFound(keyStoreKeyAlias) - } - - val cipher = Cipher.getInstance("RSA/ECB/OAEPPadding") - cipher.init( - Cipher.ENCRYPT_MODE, - privateKeyEntry.certificate.publicKey, - OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT) - ) - return cipher.doFinal(secretMessage.toByteArray()) - } - - override fun decryptTextWithWrappingKey(keyStoreKeyAlias: String, messageTekEncryptedAtClient: ByteArray): String { - val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null, null) - - val privateKeyEntry: KeyStore.PrivateKeyEntry? = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.PrivateKeyEntry? - if (privateKeyEntry == null) { - throw DomainException.noSuchImportedKeyFound(keyStoreKeyAlias) - } - - val cipher = Cipher.getInstance("RSA/ECB/OAEPPadding") - cipher.init( - Cipher.DECRYPT_MODE, - privateKeyEntry.privateKey, - OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT) - ) - return String(cipher.doFinal(messageTekEncryptedAtClient)) - } - - override fun encryptTextWithImportedKey(keyStoreKeyAlias: String, secretMessage: String): ByteArray { - val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") - keyStore.load(null, null) - val rawKey: Key? = keyStore.getKey(keyStoreKeyAlias, null) - if (rawKey == null) { - throw DomainException.noSuchImportedKeyFound(keyStoreKeyAlias) - } - val key: SecretKey = rawKey as SecretKey - - val c: Cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") - c.init(Cipher.ENCRYPT_MODE, key) - return c.iv + c.doFinal(secretMessage.toByteArray()) - } - - @Suppress("MagicNumber") - override fun decryptJWEWithImportedWrappedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String { + override fun decryptJWEWithImportedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry @@ -235,6 +168,26 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { return jweObject.payload.toString() } + override fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String): String { + val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) + keyStore.load(null, null) + val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry + + val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + // Set the message as payload plain text + val payload = Payload(message) + + // Create the JWE object and encrypt it + val jweObject = JWEObject(header, payload) + val encrypter = DirectEncrypter(secretKeyEntry.secretKey) + encrypter.jcaContext.provider = keyStore.provider + + jweObject.encrypt(encrypter) + + // Serialise to compact JOSE form + return jweObject.serialize() + } + @Suppress("MagicNumber", "unused") private fun generateFakeDecryptionKey(@Suppress("SameParameterValue") keySize: Int): SecretKey { val arraySize = keySize / 8 diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt index eb860b1..045cb61 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt @@ -5,6 +5,7 @@ import com.nimbusds.jose.JWEAlgorithm import com.nimbusds.jose.JWEHeader import com.nimbusds.jose.JWEObject import com.nimbusds.jose.Payload +import com.nimbusds.jose.crypto.DirectDecrypter import com.nimbusds.jose.crypto.DirectEncrypter import com.nimbusds.jose.jwk.JWK import mobi.lab.keyimportdemo.domain.gateway.CryptoServerGateway @@ -158,10 +159,12 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { return jweObject.serialize() } - override fun encryptMessageWithTek(message: String, tekAesKeyAtServer: SecretKeySpec): ByteArray { - val c: Cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") - c.init(Cipher.ENCRYPT_MODE, tekAesKeyAtServer) - return c.iv + c.doFinal(message.toByteArray()) + override fun decryptJWEWithImportedKey(messageWrappedTekEncryptedJWE: String, tekAesKeyAtServer: SecretKeySpec): String { + val decrypter = DirectDecrypter(tekAesKeyAtServer) + + val jweObject = JWEObject.parse(messageWrappedTekEncryptedJWE) + jweObject.decrypt(decrypter) + return jweObject.payload.toString() } companion object { diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/di/StorageModule.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/di/StorageModule.kt index a9fb34c..c1e7bbc 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/di/StorageModule.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/di/StorageModule.kt @@ -2,7 +2,9 @@ package mobi.lab.keyimportdemo.infrastructure.di import dagger.Binds import dagger.Module +import mobi.lab.keyimportdemo.domain.storage.ServerSecretKeyStorage import mobi.lab.keyimportdemo.domain.storage.SessionStorage +import mobi.lab.keyimportdemo.infrastructure.auth.local.ServerSecretKeyPreferenceStorage import mobi.lab.keyimportdemo.infrastructure.auth.local.SessionPreferenceStorage /** @@ -13,6 +15,9 @@ object StorageModule { @Module internal interface Definitions { - @Binds fun bindSessionStorage(impl: SessionPreferenceStorage): SessionStorage + @Binds + fun bindSessionStorage(impl: SessionPreferenceStorage): SessionStorage + @Binds + fun bindServerSecretKeyStorage(impl: ServerSecretKeyPreferenceStorage): ServerSecretKeyStorage } } diff --git a/app/src/main/java/mobi/lab/keyimportdemo/App.kt b/app/src/main/java/mobi/lab/keyimportdemo/App.kt index 0c42d7c..adfdd74 100644 --- a/app/src/main/java/mobi/lab/keyimportdemo/App.kt +++ b/app/src/main/java/mobi/lab/keyimportdemo/App.kt @@ -9,13 +9,11 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import io.reactivex.rxjava3.plugins.RxJavaPlugins import mobi.lab.keyimportdemo.common.logging.CrashlyticsTree import mobi.lab.keyimportdemo.common.logging.ScrollsTree -import mobi.lab.keyimportdemo.common.platform.LogoutMonitor import mobi.lab.keyimportdemo.common.platform.MyNotificationManager import mobi.lab.keyimportdemo.common.util.isCrashlyticsEnabled import mobi.lab.keyimportdemo.common.util.isDebugBuild import mobi.lab.keyimportdemo.di.Injector import mobi.lab.keyimportdemo.domain.storage.SessionStorage -import mobi.lab.keyimportdemo.domain.usecases.auth.DeleteSessionUseCase import mobi.lab.keyimportdemo.infrastructure.common.platform.AppEnvironment import mobi.lab.keyimportdemo.infrastructure.common.platform.ImageLoader import mobi.lab.scrolls.LogPost @@ -28,7 +26,6 @@ class App : Application() { @Inject lateinit var environment: AppEnvironment @Inject lateinit var sessionStorage: SessionStorage - @Inject lateinit var deleteSessionUseCase: DeleteSessionUseCase @SuppressLint("LogNotTimber") override fun onCreate() { @@ -48,7 +45,6 @@ class App : Application() { Injector.buildGraph(this) Injector.inject(this) - LogoutMonitor.init(this, deleteSessionUseCase) MyNotificationManager.newInstance(this).createChannels() ImageLoader.configure(this, environment, sessionStorage) } diff --git a/app/src/main/java/mobi/lab/keyimportdemo/common/platform/LogoutMonitor.kt b/app/src/main/java/mobi/lab/keyimportdemo/common/platform/LogoutMonitor.kt deleted file mode 100644 index 67f0729..0000000 --- a/app/src/main/java/mobi/lab/keyimportdemo/common/platform/LogoutMonitor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package mobi.lab.keyimportdemo.common.platform - -import android.app.Application -import mobi.lab.keyimportdemo.common.util.NavUtil -import mobi.lab.keyimportdemo.domain.usecases.auth.DeleteSessionUseCase -import timber.log.Timber - -/** - * Helps to handle automatic logouts. The automatic logout is locked until we are sure that we have logged back in. - * logout() is called whenever a network request fails because of an unauthorized session. - * Needs to be a singleton (object) to enforce global syncing of logout requests - * - * reset() is called in 2 places: - * MainActivity.onCreate(): - * - Main application screen => We have a proper session and are ready to handle any errors - * PickCompanyFragment.onCreate(): - * - During login we might see this screen before MainActivity => Let's be ready to handle errors - */ -object LogoutMonitor { - - @Volatile private lateinit var application: Application - @Volatile private var logoutHandled = false - @Volatile private lateinit var deleteSessionUseCase: DeleteSessionUseCase - - fun init(application: Application, deleteSessionUseCase: DeleteSessionUseCase) { - this.application = application - this.deleteSessionUseCase = deleteSessionUseCase - } - - /** - * Call this once the application knows that the user is in a logged in state - * and can start interact with the application. The main starting screen for example. - */ - fun reset() { - synchronized(this) { - logoutHandled = false - } - } - - /** - * Call this once the application knows that the session is not valid - */ - fun logout() { - synchronized(this) { - if (logoutHandled) { - Timber.e("logout called, already called, ignore") - return - } - - logoutHandled = true - deleteSessionUseCase.execute() - NavUtil.restartApplication(application) - } - } -} diff --git a/app/src/main/java/mobi/lab/keyimportdemo/common/rx/SchedulerProvider.kt b/app/src/main/java/mobi/lab/keyimportdemo/common/rx/SchedulerProvider.kt index 3fd6aee..8c8f33c 100644 --- a/app/src/main/java/mobi/lab/keyimportdemo/common/rx/SchedulerProvider.kt +++ b/app/src/main/java/mobi/lab/keyimportdemo/common/rx/SchedulerProvider.kt @@ -5,9 +5,6 @@ import io.reactivex.rxjava3.core.MaybeTransformer import io.reactivex.rxjava3.core.ObservableTransformer import io.reactivex.rxjava3.core.Scheduler import io.reactivex.rxjava3.core.SingleTransformer -import mobi.lab.keyimportdemo.common.platform.LogoutMonitor -import mobi.lab.keyimportdemo.domain.entities.DomainException -import mobi.lab.keyimportdemo.domain.entities.ErrorCode interface SchedulerProvider { val main: Scheduler @@ -54,8 +51,6 @@ interface SchedulerProvider { } private fun checkUnauthorizedError(error: Throwable) { - if (error is DomainException && error.isFor(ErrorCode.LOCAL_UNAUTHORIZED)) { - LogoutMonitor.logout() - } + // No-OP } } diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt index 04ce8b0..adaf84c 100644 --- a/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt +++ b/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt @@ -47,7 +47,6 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder initToolbar(this) buttonStartTest.setOnClickListener { viewModel.onRunKeyImportTestClicked() } buttonStartTestImportKeyUsage.setOnClickListener { viewModel.onRunImportKeyUsageTestClicked() } - buttonStartTestWrappingKeyUsage.setOnClickListener { viewModel.onRunWrappingKeyUsageTestClicked() } } initViewModel() @@ -64,18 +63,16 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder private fun initViewModelState() { viewModel.state.onEachNotNull { state -> when (state.status) { - is TestStatus.FailedGeneric -> onKeyImportFailedGeneric(state.status) - TestStatus.InProgress -> onTestStarted() - TestStatus.NotStated -> onTestEnded() - is TestStatus.SuccessStrongboxTee -> onKeyImportSuccessStongboxTee(state.status) - is TestStatus.SuccessHardwareTeeNoStrongbox -> onKeyImportSuccessHardwareTeeNoStrongbox(state.status) - is TestStatus.SuccessSoftwareTeeOnly -> onKeyImportSuccessSoftwareTeeOnly(state.status) - TestStatus.FailedKeyImportNotSupportedOnThisApiLevel -> onKeyImportFailedNotSupportedOnThisApiLevel(state.status) - TestStatus.FailedKeyImportNotAvailableOnThisDevice -> onKeyImportFailedNotAvailableOnThisDevice(state.status) - is TestStatus.FailedTestDecryptionResultDifferentThanInput -> onDecryptFailed(state.status) - is TestStatus.SuccessUnknownTeeOnly -> onKeyImportSuccessUnknownTee(state.status) - TestStatus.FailedLocalKeyUsageNoSuchKey -> onLocalKeyUsageNoSuchKey(state.status) - is TestStatus.SuccessLocalKeyUsage -> onLocalKeyUsageSuccess(state.status) + is UiTestStatus.FailedImportGeneric -> onKeyImportFailedGeneric(state.status) + is UiTestStatus.FailedUsageGeneric -> onKeyUsageFailedGeneric(state.status) + UiTestStatus.InProgress -> onTestStarted() + UiTestStatus.NotStated -> onTestEnded() + is UiTestStatus.SuccessKeyImportAndUsage -> onKeyImportSuccess(state.status) + UiTestStatus.FailedKeyImportNotSupportedOnThisApiLevel -> onKeyImportFailedNotSupportedOnThisApiLevel(state.status) + UiTestStatus.FailedKeyImportNotAvailableOnThisDevice -> onKeyImportFailedNotAvailableOnThisDevice(state.status) + is UiTestStatus.FailedTestDecryptionResultDifferentThanInput -> onDecryptFailed(state.status) + UiTestStatus.FailedKeyUsageNoSuchKey -> onLocalKeyUsageNoSuchKey(state.status) + is UiTestStatus.SuccessKeyUsage -> onKeyUsageSuccess(state.status) }.exhaustive onLogLinesUpdated(state.logLines) } @@ -110,32 +107,37 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder return logLines.joinToString(separator = "\n") } - private fun getResultAsText(status: TestStatus): String { + private fun getResultAsText(status: UiTestStatus): String { return when (status) { - is TestStatus.FailedGeneric -> - getString(R.string.text_status_failed) - TestStatus.FailedKeyImportNotAvailableOnThisDevice -> + is UiTestStatus.FailedImportGeneric -> + getString(R.string.text_status_key_import_failed) + is UiTestStatus.FailedUsageGeneric -> + getString(R.string.text_status_key_usage_failed) + UiTestStatus.FailedKeyImportNotAvailableOnThisDevice -> getString(R.string.text_status_failed_no_key_import_support_available_on_this_device) - TestStatus.FailedKeyImportNotSupportedOnThisApiLevel -> + UiTestStatus.FailedKeyImportNotSupportedOnThisApiLevel -> getString(R.string.text_status_failed_no_key_import_support_on_this_api_level) - TestStatus.FailedTestDecryptionResultDifferentThanInput -> + UiTestStatus.FailedTestDecryptionResultDifferentThanInput -> getString(R.string.text_status_decrypt_failed) - TestStatus.InProgress -> + UiTestStatus.InProgress -> getString(R.string.text_status_test_in_progress) - TestStatus.NotStated -> + UiTestStatus.NotStated -> getString(R.string.text_status_test_not_started) - is TestStatus.SuccessHardwareTeeNoStrongbox -> - getString(R.string.text_status_success_hardware_tee_no_strongbox) - is TestStatus.SuccessSoftwareTeeOnly -> - getString(R.string.text_status_success_software_tee_only) - is TestStatus.SuccessStrongboxTee -> - getString(R.string.text_status_success_strongbox_tee) - is TestStatus.SuccessUnknownTeeOnly -> - getString(R.string.text_status_success_unknown_tee) - TestStatus.FailedLocalKeyUsageNoSuchKey -> + is UiTestStatus.SuccessKeyUsage -> + getString(R.string.text_status_success_key_usage) + " " + getResultAsText(status.keyTeeLevel) + is UiTestStatus.SuccessKeyImportAndUsage -> + getString(R.string.text_status_key_import_success) + " " + getResultAsText(status.keyUsageStatus) + UiTestStatus.FailedKeyUsageNoSuchKey -> getString(R.string.text_status_local_key_usage_failed_no_such_key) - is TestStatus.SuccessLocalKeyUsage -> - getString(R.string.text_status_success_local_key_usage) + }.exhaustive + } + + private fun getResultAsText(teeLevel: UIKeyTeeSecurityLevel): String { + return when (teeLevel) { + UIKeyTeeSecurityLevel.TeeHardwareNoStrongbox -> getString(R.string.text_key_level_hardware_tee_no_strongbox) + UIKeyTeeSecurityLevel.TeeSoftware -> getString(R.string.text_key_level_software_tee_only) + UIKeyTeeSecurityLevel.TeeStrongbox -> getString(R.string.text_key_level_hardware_strongboc_tee) + UIKeyTeeSecurityLevel.Unknown -> getString(R.string.text_key_level_unknown_tee) }.exhaustive } @@ -202,55 +204,45 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder } } - private fun onKeyImportSuccessStongboxTee(status: TestStatus) { + private fun onKeyImportSuccess(status: UiTestStatus) { + if (status is UiTestStatus.SuccessKeyImportAndUsage) { + if (status.keyUsageStatus !is UiTestStatus.SuccessKeyUsage) { + displayTestFailed(status) + return + } + } displayTestFullSuccess(status) } - private fun onKeyImportSuccessHardwareTeeNoStrongbox(status: TestStatus) { + private fun onKeyUsageSuccess(status: UiTestStatus) { displayTestFullSuccess(status) } - private fun onKeyImportSuccessSoftwareTeeOnly(status: TestStatus) { - displayTestPartialSuccess(status) - } - - private fun onKeyImportSuccessUnknownTee(status: TestStatus) { - displayTestPartialSuccess(status) - } - - private fun onLocalKeyUsageSuccess(status: TestStatus) { - displayTestFullSuccess(status) - } - - private fun onLocalKeyUsageNoSuchKey(status: TestStatus) { + private fun onLocalKeyUsageNoSuchKey(status: UiTestStatus) { displayTestFailed(status) } - private fun onKeyImportFailedGeneric(status: TestStatus) { + private fun onKeyImportFailedGeneric(status: UiTestStatus) { displayTestFailed(status) } - private fun onDecryptFailed(status: TestStatus) { + private fun onKeyUsageFailedGeneric(status: UiTestStatus) { displayTestFailed(status) } - private fun onKeyImportFailedNotSupportedOnThisApiLevel(status: TestStatus) { + private fun onDecryptFailed(status: UiTestStatus) { displayTestFailed(status) } - private fun onKeyImportFailedNotAvailableOnThisDevice(status: TestStatus) { + private fun onKeyImportFailedNotSupportedOnThisApiLevel(status: UiTestStatus) { displayTestFailed(status) } - private fun displayTestPartialSuccess(status: TestStatus) { - onTestEnded() - requireBinding { - textTitle.text = getResultAsText(status) - textTitle.setBackgroundColor(resources.getColor(R.color.yellow, requireContext().theme)) - } + private fun onKeyImportFailedNotAvailableOnThisDevice(status: UiTestStatus) { + displayTestFailed(status) } - private fun displayTestFullSuccess(status: TestStatus) { + private fun displayTestFullSuccess(status: UiTestStatus) { onTestEnded() requireBinding { textTitle.text = getResultAsText(status) @@ -258,7 +250,7 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder } } - private fun displayTestFailed(status: TestStatus) { + private fun displayTestFailed(status: UiTestStatus) { onTestEnded() requireBinding { textTitle.text = getResultAsText(status) diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/MainViewModel.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/MainViewModel.kt index ffdbdce..23a59fd 100644 --- a/app/src/main/java/mobi/lab/keyimportdemo/main/MainViewModel.kt +++ b/app/src/main/java/mobi/lab/keyimportdemo/main/MainViewModel.kt @@ -11,11 +11,11 @@ import mobi.lab.keyimportdemo.common.rx.SchedulerProvider import mobi.lab.keyimportdemo.common.util.asLiveData import mobi.lab.keyimportdemo.common.util.dispose import mobi.lab.keyimportdemo.domain.entities.KeyImportTestResult -import mobi.lab.keyimportdemo.domain.entities.KeyLocalUsageTestResult +import mobi.lab.keyimportdemo.domain.entities.KeyUsageTestResult +import mobi.lab.keyimportdemo.domain.gateway.CryptoClientGateway import mobi.lab.keyimportdemo.domain.gateway.LoggerGateway +import mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyTwoWayUsageUseCase import mobi.lab.keyimportdemo.domain.usecases.crypto.KeyImportUseCase -import mobi.lab.keyimportdemo.domain.usecases.crypto.ImportedKeyLocalUsageUseCase -import mobi.lab.keyimportdemo.domain.usecases.crypto.WrappingKeyLocalUsageUseCase import mobi.lab.mvvm.SingleEvent import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -23,8 +23,7 @@ import javax.inject.Inject class MainViewModel @Inject constructor( private val keyImportUseCase: KeyImportUseCase, - private val importedKeyLocalUsageUseCase: ImportedKeyLocalUsageUseCase, - private val wrappingKeyLocalUsageUseCase: WrappingKeyLocalUsageUseCase, + private val importedKeyTwoWayUsageUseCase: ImportedKeyTwoWayUsageUseCase, private val schedulers: SchedulerProvider, private val logger: LoggerGateway ) : BaseViewModel(), LoggerGateway.LoggerGatewayListener { @@ -49,17 +48,17 @@ class MainViewModel @Inject constructor( fun onRunKeyImportTestClicked() { logger.clearLog() - updateState { it.copy(status = TestStatus.InProgress) } + updateState { it.copy(status = UiTestStatus.InProgress) } dispose(disposable) disposable = keyImportUseCase - .execute("Hello world! The date and time is ${formatCurrentTime()}") + .execute(createServerMessage(), createClientMessage()) .compose(schedulers.single(Schedulers.computation(), AndroidSchedulers.mainThread())) .subscribe(::onKeyImportTestSuccess, ::onKeyImportTestFailed) } private fun onKeyImportTestFailed(error: Throwable) { logger.d("onKeyImportTestFailed: ${Log.getStackTraceString(error)}") - updateState { it.copy(status = TestStatus.FailedGeneric(error)) } + updateState { it.copy(status = UiTestStatus.FailedImportGeneric(error)) } } private fun onKeyImportTestSuccess(result: KeyImportTestResult) { @@ -69,30 +68,24 @@ class MainViewModel @Inject constructor( fun onRunImportKeyUsageTestClicked() { logger.clearLog() - updateState { it.copy(status = TestStatus.InProgress) } + updateState { it.copy(status = UiTestStatus.InProgress) } dispose(disposable) - disposable = importedKeyLocalUsageUseCase - .execute("Hello world! The date and time is ${formatCurrentTime()}") + disposable = importedKeyTwoWayUsageUseCase + .execute(createServerMessage(), createClientMessage()) .compose(schedulers.single(Schedulers.computation(), AndroidSchedulers.mainThread())) .subscribe(::onKeyLocalUsageTestSuccess, ::onKeyLocalUsageTestFailed) } - fun onRunWrappingKeyUsageTestClicked() { - logger.clearLog() - updateState { it.copy(status = TestStatus.InProgress) } - dispose(disposable) - disposable = wrappingKeyLocalUsageUseCase - .execute("Hello world! The date and time is ${formatCurrentTime()}") - .compose(schedulers.single(Schedulers.computation(), AndroidSchedulers.mainThread())) - .subscribe(::onKeyLocalUsageTestSuccess, ::onKeyLocalUsageTestFailed) - } + private fun createServerMessage() = "Hello from Server! The date and time is ${formatCurrentTime()}" + + private fun createClientMessage() = "Hello from Client! The date and time is ${formatCurrentTime()}" private fun onKeyLocalUsageTestFailed(error: Throwable) { logger.d("onKeyLocalUsageTestFailed: ${Log.getStackTraceString(error)}") - updateState { it.copy(status = TestStatus.FailedGeneric(error)) } + updateState { it.copy(status = UiTestStatus.FailedUsageGeneric(error)) } } - private fun onKeyLocalUsageTestSuccess(result: KeyLocalUsageTestResult) { + private fun onKeyLocalUsageTestSuccess(result: KeyUsageTestResult) { logger.d("onKeyLocalUsageTestSuccess: $result") updateState { it.copy(status = mapResult(result)) } } @@ -111,21 +104,26 @@ class MainViewModel @Inject constructor( } private fun startShare() { - when (state.value?.status ?: TestStatus.NotStated) { - is TestStatus.FailedGeneric, - TestStatus.FailedKeyImportNotAvailableOnThisDevice, - TestStatus.FailedKeyImportNotSupportedOnThisApiLevel, - TestStatus.FailedLocalKeyUsageNoSuchKey, - TestStatus.FailedTestDecryptionResultDifferentThanInput -> { + val status = state.value?.status ?: UiTestStatus.NotStated + when (status) { + is UiTestStatus.FailedImportGeneric, + is UiTestStatus.FailedUsageGeneric, + UiTestStatus.FailedKeyImportNotAvailableOnThisDevice, + UiTestStatus.FailedKeyImportNotSupportedOnThisApiLevel, + UiTestStatus.FailedKeyUsageNoSuchKey, + UiTestStatus.FailedTestDecryptionResultDifferentThanInput -> { shareResultAndLog(state.value ?: defaultState()) } - TestStatus.InProgress, - TestStatus.NotStated, - is TestStatus.SuccessHardwareTeeNoStrongbox, - is TestStatus.SuccessSoftwareTeeOnly, - is TestStatus.SuccessStrongboxTee, - is TestStatus.SuccessLocalKeyUsage, - is TestStatus.SuccessUnknownTeeOnly -> { + UiTestStatus.InProgress, + UiTestStatus.NotStated, + is UiTestStatus.SuccessKeyUsage -> { + shareResultOnly(state.value ?: defaultState()) + } + is UiTestStatus.SuccessKeyImportAndUsage -> { + if (status.keyUsageStatus !is UiTestStatus.SuccessKeyUsage) { + shareResultAndLog(state.value ?: defaultState()) + return + } shareResultOnly(state.value ?: defaultState()) } }.exhaustive @@ -140,7 +138,7 @@ class MainViewModel @Inject constructor( } private fun defaultState(): State { - return State(TestStatus.NotStated, ArrayList()) + return State(UiTestStatus.NotStated, ArrayList()) } private fun updateState(function: (State) -> State) { @@ -151,23 +149,33 @@ class MainViewModel @Inject constructor( return _state.value ?: defaultState() } - private fun mapResult(result: KeyImportTestResult): TestStatus { + private fun mapResult(result: KeyImportTestResult): UiTestStatus { return when (result) { - KeyImportTestResult.FailedKeyImportNotSupportedOnThisApiLevel -> TestStatus.FailedKeyImportNotSupportedOnThisApiLevel - KeyImportTestResult.FailedKeyImportNotAvailableOnThisDevice -> TestStatus.FailedKeyImportNotAvailableOnThisDevice - KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput -> TestStatus.FailedTestDecryptionResultDifferentThanInput - is KeyImportTestResult.SuccessHardwareTeeStrongBox -> TestStatus.SuccessStrongboxTee(result.message) - is KeyImportTestResult.SuccessHardwareTeeNoStrongbox -> TestStatus.SuccessHardwareTeeNoStrongbox(result.message) - is KeyImportTestResult.SuccessSoftwareTeeOnly -> TestStatus.SuccessSoftwareTeeOnly(result.message) - is KeyImportTestResult.SuccessTeeUnknown -> TestStatus.SuccessUnknownTeeOnly(result.message) + KeyImportTestResult.FailedKeyImportNotSupportedOnThisApiLevel -> UiTestStatus.FailedKeyImportNotSupportedOnThisApiLevel + KeyImportTestResult.FailedKeyImportNotAvailableOnThisDevice -> UiTestStatus.FailedKeyImportNotAvailableOnThisDevice + KeyImportTestResult.FailedTestDecryptionResultDifferentThanInput -> UiTestStatus.FailedTestDecryptionResultDifferentThanInput + is KeyImportTestResult.Success -> UiTestStatus.SuccessKeyImportAndUsage(mapResult(result.keyTestResult)) }.exhaustive } - private fun mapResult(result: KeyLocalUsageTestResult): TestStatus { + private fun mapResult(result: KeyUsageTestResult): UiTestStatus { return when (result) { - is KeyLocalUsageTestResult.UsageFailedGeneric -> TestStatus.FailedGeneric(result.throwable) - KeyLocalUsageTestResult.UsageFailedNoSuchKey -> TestStatus.FailedLocalKeyUsageNoSuchKey - is KeyLocalUsageTestResult.UsageSuccess -> TestStatus.SuccessLocalKeyUsage(result.message) + is KeyUsageTestResult.UsageFailedGeneric -> UiTestStatus.FailedUsageGeneric(result.throwable) + KeyUsageTestResult.UsageFailedNoSuchKey -> UiTestStatus.FailedKeyUsageNoSuchKey + is KeyUsageTestResult.UsageSuccess -> UiTestStatus.SuccessKeyUsage( + mapResult(result.keyLevel), + result.serverToClientMessage, + result.clientToServerMessage + ) + }.exhaustive + } + + private fun mapResult(keyLevel: CryptoClientGateway.KeyTeeSecurityLevel): UIKeyTeeSecurityLevel { + return when (keyLevel) { + CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox -> UIKeyTeeSecurityLevel.TeeHardwareNoStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware -> UIKeyTeeSecurityLevel.TeeSoftware + CryptoClientGateway.KeyTeeSecurityLevel.TeeStrongbox -> UIKeyTeeSecurityLevel.TeeStrongbox + CryptoClientGateway.KeyTeeSecurityLevel.Unknown -> UIKeyTeeSecurityLevel.Unknown }.exhaustive } @@ -178,7 +186,7 @@ class MainViewModel @Inject constructor( data class ShareResultAndLog(val state: State) : Action() } - data class State(val status: TestStatus, val logLines: List) + data class State(val status: UiTestStatus, val logLines: List) override fun onLogLinesUpdated(logLines: List) { updateState { it.copy(logLines = ArrayList(logLines)) } diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/TestStatus.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/TestStatus.kt deleted file mode 100644 index f2627b0..0000000 --- a/app/src/main/java/mobi/lab/keyimportdemo/main/TestStatus.kt +++ /dev/null @@ -1,75 +0,0 @@ -package mobi.lab.keyimportdemo.main - -sealed class TestStatus { - object NotStated : TestStatus() { - override fun toString(): String { - return "NotStated" - } - } - - object InProgress : TestStatus() { - override fun toString(): String { - return "InProgress" - } - } - - object FailedKeyImportNotSupportedOnThisApiLevel : TestStatus() { - override fun toString(): String { - return "FailedKeyImportNotSupportedOnThisApiLevel" - } - } - - object FailedKeyImportNotAvailableOnThisDevice : TestStatus() { - override fun toString(): String { - return "FailedKeyImportNotAvailableOnThisDevice" - } - } - - object FailedTestDecryptionResultDifferentThanInput : TestStatus() { - override fun toString(): String { - return "FailedTestDecryptionResultDifferentThanInput" - } - } - - data class FailedGeneric(val error: Throwable) : TestStatus() { - override fun toString(): String { - return "FailedGeneric" - } - } - - data class SuccessStrongboxTee(val message: String) : TestStatus() { - override fun toString(): String { - return "SuccessStrongboxTee" - } - } - - data class SuccessHardwareTeeNoStrongbox(val message: String) : TestStatus() { - override fun toString(): String { - return "SuccessHardwareTeeNoStrongbox" - } - } - - data class SuccessSoftwareTeeOnly(val message: String) : TestStatus() { - override fun toString(): String { - return "SuccessSoftwareTeeOnly" - } - } - - data class SuccessUnknownTeeOnly(val message: String) : TestStatus() { - override fun toString(): String { - return "SuccessUnknownTeeOnly" - } - } - - data class SuccessLocalKeyUsage(val message: String) : TestStatus() { - override fun toString(): String { - return "SuccessLocalKeyUsage" - } - } - - object FailedLocalKeyUsageNoSuchKey : TestStatus() { - override fun toString(): String { - return "FailedLocalKeyUsageNoSuchKey" - } - } -} diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/UIKeyTeeSecurityLevel.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/UIKeyTeeSecurityLevel.kt new file mode 100644 index 0000000..7e11588 --- /dev/null +++ b/app/src/main/java/mobi/lab/keyimportdemo/main/UIKeyTeeSecurityLevel.kt @@ -0,0 +1,27 @@ +package mobi.lab.keyimportdemo.main + +sealed class UIKeyTeeSecurityLevel { + object TeeStrongbox : UIKeyTeeSecurityLevel() { + override fun toString(): String { + return "UIKeyTeeSecurityLevel.TeeStrongbox" + } + } + + object TeeHardwareNoStrongbox : UIKeyTeeSecurityLevel() { + override fun toString(): String { + return "UIKeyTeeSecurityLevel.TeeHardwareNoStrongbox" + } + } + + object TeeSoftware : UIKeyTeeSecurityLevel() { + override fun toString(): String { + return "UIKeyTeeSecurityLevel.TeeSoftware" + } + } + + object Unknown : UIKeyTeeSecurityLevel() { + override fun toString(): String { + return "UIKeyTeeSecurityLevel.Unknown" + } + } +} diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/UiTestStatus.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/UiTestStatus.kt new file mode 100644 index 0000000..c4d06dd --- /dev/null +++ b/app/src/main/java/mobi/lab/keyimportdemo/main/UiTestStatus.kt @@ -0,0 +1,63 @@ +package mobi.lab.keyimportdemo.main + +sealed class UiTestStatus { + object NotStated : UiTestStatus() { + override fun toString(): String { + return "NotStated" + } + } + + object InProgress : UiTestStatus() { + override fun toString(): String { + return "InProgress" + } + } + + object FailedKeyImportNotSupportedOnThisApiLevel : UiTestStatus() { + override fun toString(): String { + return "FailedKeyImportNotSupportedOnThisApiLevel" + } + } + + object FailedKeyImportNotAvailableOnThisDevice : UiTestStatus() { + override fun toString(): String { + return "FailedKeyImportNotAvailableOnThisDevice" + } + } + + object FailedTestDecryptionResultDifferentThanInput : UiTestStatus() { + override fun toString(): String { + return "FailedTestDecryptionResultDifferentThanInput" + } + } + + data class FailedImportGeneric(val error: Throwable) : UiTestStatus() { + override fun toString(): String { + return "FailedImportGeneric" + } + } + + data class FailedUsageGeneric(val error: Throwable) : UiTestStatus() { + override fun toString(): String { + return "FailedUsageGeneric" + } + } + + data class SuccessKeyImportAndUsage(val keyUsageStatus: UiTestStatus) : UiTestStatus() { + override fun toString(): String { + return "SuccessKeyImportAndUsage(keyUsageStatus=$keyUsageStatus)" + } + } + + data class SuccessKeyUsage(val keyTeeLevel: UIKeyTeeSecurityLevel, val serverMessage: String, val clientMessage: String) : UiTestStatus() { + override fun toString(): String { + return "SuccessKeyUsage(keyTeeLevel=$keyTeeLevel, serverMessage='$serverMessage', clientMessage='$clientMessage')" + } + } + + object FailedKeyUsageNoSuchKey : UiTestStatus() { + override fun toString(): String { + return "FailedKeyUsageNoSuchKey" + } + } +} diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index e607ba4..3ac0e8a 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -37,7 +37,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/guide_end" app:layout_constraintStart_toStartOf="@id/guide_start" - app:layout_constraintTop_toBottomOf="@id/button_start_test_wrapping_key_usage"> + app:layout_constraintTop_toBottomOf="@id/button_start_test_import_key_usage"> - - + app:layout_constraintTop_toBottomOf="@id/button_start_test" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 539eced..b6a5288 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,9 +10,8 @@ Required Key Import Demo Tap on the button below to run the key import and Hello World message decryption test. - Run full key import test - Use imported key - Use wrapping key + Run full key import test with key usage + Run imported key usage only Log out Debug menu @@ -22,9 +21,9 @@ Import in progress .. Import success: Hardware Strongbox TEE Import success: Hardware TEE (not Strongbox) - Import success: Software TEE only + Key Import success. Import success: Unknown TEE only - Import failed! See the trace below. + Import failed! See the trace below. Test failed. Decryption result different, see trace. Import failed: No import support on this API level Import failed: No import support available on this device @@ -32,6 +31,11 @@ Share Key Import Demo results Test in progress .. Test not started! - Local key usage successful! + Key usage success. Failed: No such key. Was it not generated or removed? + Hardware TEE No Strongbox + Software TEE only + Hardware StrongBox TEE + Unknown TEE + Key usage failed! See the trace below. From a6abd031a02af983de4d08f5e6183263954e1930 Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Thu, 22 Sep 2022 16:29:37 +0300 Subject: [PATCH 3/7] 1: Do cleanup --- .../usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt | 11 +++++++---- .../java/mobi/lab/keyimportdemo/main/MainFragment.kt | 2 +- app/src/main/res/values/strings.xml | 7 ++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt index b14a5bb..dc9363d 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt @@ -26,17 +26,20 @@ class ImportedKeyTwoWayUsageUseCase @Inject constructor( } private fun runTest(serverToClientSecretMessage: String, clientToServerSecretMessage: String): KeyUsageTestResult { - log.d("Local usage test started") + log.d("Usage test started") return try { - log.d("Testing server -> client message flow ..") - val serverToClientSecretMessageResult = runTestServerEncryptCryptAndClientDecryptJweWithTek(serverToClientSecretMessage) - log.d("Testing server -> client message flow test finished") log.d("Testing client -> server message flow ..") val clientToServerSecretMessageResult = runTestClientEncryptCryptAndServerDecryptJweWithTek(clientToServerSecretMessage) log.d("Testing client -> server message flow test finished") + log.d("Testing server -> client message flow ..") + val serverToClientSecretMessageResult = runTestServerEncryptCryptAndClientDecryptJweWithTek(serverToClientSecretMessage) + log.d("Testing server -> client message flow test finished") + + + KeyUsageTestResult.UsageSuccess( client.getPrivateKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS), serverToClientSecretMessageResult, diff --git a/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt b/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt index adaf84c..1ae46ef 100644 --- a/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt +++ b/app/src/main/java/mobi/lab/keyimportdemo/main/MainFragment.kt @@ -136,7 +136,7 @@ class MainFragment : BaseMvvmFragment(), ViewBindingHolder return when (teeLevel) { UIKeyTeeSecurityLevel.TeeHardwareNoStrongbox -> getString(R.string.text_key_level_hardware_tee_no_strongbox) UIKeyTeeSecurityLevel.TeeSoftware -> getString(R.string.text_key_level_software_tee_only) - UIKeyTeeSecurityLevel.TeeStrongbox -> getString(R.string.text_key_level_hardware_strongboc_tee) + UIKeyTeeSecurityLevel.TeeStrongbox -> getString(R.string.text_key_level_hardware_strongbox_tee) UIKeyTeeSecurityLevel.Unknown -> getString(R.string.text_key_level_unknown_tee) }.exhaustive } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6a5288..36fcc1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,10 +19,7 @@ Connection error. Please try again. Click to start Import in progress .. - Import success: Hardware Strongbox TEE - Import success: Hardware TEE (not Strongbox) Key Import success. - Import success: Unknown TEE only Import failed! See the trace below. Test failed. Decryption result different, see trace. Import failed: No import support on this API level @@ -32,10 +29,10 @@ Test in progress .. Test not started! Key usage success. - Failed: No such key. Was it not generated or removed? + Failed: No such key. Not generated? Removed? Hardware TEE No Strongbox Software TEE only - Hardware StrongBox TEE + Hardware StrongBox TEE Unknown TEE Key usage failed! See the trace below. From 7c273b9995fc9e3cbce5d3191395e240ce07e4a4 Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Fri, 23 Sep 2022 13:58:01 +0300 Subject: [PATCH 4/7] 1: Use newer Nimbus library --- .../usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt | 8 +++----- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt index dc9363d..f60b0af 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt @@ -30,15 +30,13 @@ class ImportedKeyTwoWayUsageUseCase @Inject constructor( return try { - log.d("Testing client -> server message flow ..") - val clientToServerSecretMessageResult = runTestClientEncryptCryptAndServerDecryptJweWithTek(clientToServerSecretMessage) - log.d("Testing client -> server message flow test finished") - log.d("Testing server -> client message flow ..") val serverToClientSecretMessageResult = runTestServerEncryptCryptAndClientDecryptJweWithTek(serverToClientSecretMessage) log.d("Testing server -> client message flow test finished") - + log.d("Testing client -> server message flow ..") + val clientToServerSecretMessageResult = runTestClientEncryptCryptAndServerDecryptJweWithTek(clientToServerSecretMessage) + log.d("Testing client -> server message flow test finished") KeyUsageTestResult.UsageSuccess( client.getPrivateKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46dc444..7f7abb1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,5 +89,5 @@ test-androidx-junit = 'androidx.test.ext:junit:1.1.3' test-espresso-core = 'androidx.test.espresso:espresso-core:3.4.0' # Crypto -nimbus = 'com.nimbusds:nimbus-jose-jwt:9.25.1' +nimbus = 'com.nimbusds:nimbus-jose-jwt:9.25.2' bouncycastle= 'org.bouncycastle:bcprov-jdk16:1.46' From 0d4d20b3c8e735b09b084c946ce95f9697ac338a Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Mon, 26 Sep 2022 10:57:06 +0300 Subject: [PATCH 5/7] 1: Use a newer Nimbus version --- .../lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt | 4 ++-- gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt index 9bef882..569fad5 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt @@ -161,7 +161,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry val decrypter = DirectDecrypter(secretKeyEntry.secretKey) - decrypter.jcaContext.provider = keyStore.provider + // Google does not recommend this: decrypter.jcaContext.provider = keyStore.provider val jweObject = JWEObject.parse(messageWrappedTekEncryptedJWE) jweObject.decrypt(decrypter) @@ -180,7 +180,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { // Create the JWE object and encrypt it val jweObject = JWEObject(header, payload) val encrypter = DirectEncrypter(secretKeyEntry.secretKey) - encrypter.jcaContext.provider = keyStore.provider + // Google does not recommend this: encrypter.jcaContext.provider = keyStore.provider jweObject.encrypt(encrypter) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f7abb1..05cd361 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -89,5 +89,5 @@ test-androidx-junit = 'androidx.test.ext:junit:1.1.3' test-espresso-core = 'androidx.test.espresso:espresso-core:3.4.0' # Crypto -nimbus = 'com.nimbusds:nimbus-jose-jwt:9.25.2' +nimbus = 'com.nimbusds:nimbus-jose-jwt:9.25.3' bouncycastle= 'org.bouncycastle:bcprov-jdk16:1.46' From 5f914dd4abda4bd859e2c4deba46919b3e5e3767 Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Wed, 28 Sep 2022 09:27:02 +0300 Subject: [PATCH 6/7] 1: Clean up mislabeled bit and byte values --- .../keyimportdemo/domain/DomainConstants.kt | 2 + .../domain/gateway/CryptoClientGateway.kt | 2 +- .../domain/gateway/CryptoServerGateway.kt | 6 +-- .../crypto/ImportedKeyTwoWayUsageUseCase.kt | 6 ++- .../usecases/crypto/KeyImportUseCase.kt | 8 ++-- .../infrastructure/crypto/CryptoClient.kt | 35 +++++------------- .../infrastructure/crypto/CryptoServer.kt | 37 ++++++++++--------- .../infrastructure/crypto/CryptoUtil.kt | 34 +++++++++++++++++ 8 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoUtil.kt diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/DomainConstants.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/DomainConstants.kt index 0b2e0ff..ee60b19 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/DomainConstants.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/DomainConstants.kt @@ -1,6 +1,8 @@ package mobi.lab.keyimportdemo.domain object DomainConstants { + const val TEK_KEY_SIZE_BITS = 256 + const val CEK_KEY_SIZE_BITS = 256 const val DEVICE_TEE_WRAPPING_KEY_ALIAS = "device_wrapping_key_alias" const val DEVICE_TEE_IMPORT_KEY_ALIAS = "device_wrapped_key_alias" } diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt index dac2851..dc491a0 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoClientGateway.kt @@ -10,7 +10,7 @@ interface CryptoClientGateway { fun encodeRsaPublicKeyAsJwk(alias: String, publicKey: PublicKey): String fun importWrappedKeyFromServer(asn1DerEncodedWrappedKey: ByteArray, wrappingKeyAliasInKeysStore: String, wrappedKeyAlias: String) fun decryptJWEWithImportedKey(keyStoreKeyAlias: String, messageWrappedTekEncryptedJWE: String): String - fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String): String + fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String, keySizeBits: Int): String sealed class KeyTeeSecurityLevel { object TeeStrongbox : KeyTeeSecurityLevel() { diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt index 72bd475..5d995ff 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/gateway/CryptoServerGateway.kt @@ -9,9 +9,9 @@ interface CryptoServerGateway { @Throws(Exception::class) fun decodeRsaPublicKeyFromJWKString(jwkString: String): PublicKey - fun generateAesTek(keySize: Int): SecretKeySpec - fun generateTekImportMetadata(keySizeBytes: Int, keyMasterAlgorithm: Int): DERSequence - fun generateAesCek(cekKeySizeBytes: Int): SecretKeySpec + fun generateAesTek(keySizeBits: Int): SecretKeySpec + fun generateTekImportMetadata(keySizeBits: Int, keyMasterAlgorithm: Int): DERSequence + fun generateAesCek(cekKeySizeBits: Int): SecretKeySpec fun encryptCekWithRsaPublicKey(cekAesKey: SecretKeySpec, rsaKeyPairPublicKey: PublicKey): ByteArray fun encryptTekWithCek(tekAesWrappedKey: SecretKeySpec, wrappedKeyDescription: DERSequence, cekAesKey: SecretKeySpec): EncryptedTekWrapper fun encodeTekAndCetToAsn1Der(tekEncryptedWrapper: EncryptedTekWrapper, cekEncrypted: ByteArray, tekImportMetadata: DERSequence): ByteArray diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt index f60b0af..7fc8743 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt @@ -78,7 +78,11 @@ class ImportedKeyTwoWayUsageUseCase @Inject constructor( throw DomainException.noSuchServerKeyFound(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS) } val keySpec = SecretKeySpec(keyBytes, "AES") - val encryptedSecretMessage = client.encryptMessageWithTekToJWE(secretMessage, DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS) + val encryptedSecretMessage = client.encryptMessageWithTekToJWE( + secretMessage, + DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS, + DomainConstants.TEK_KEY_SIZE_BITS + ) val resultDecryptedMessage = server.decryptJWEWithImportedKey(encryptedSecretMessage, keySpec) assertDecryptedMessageEqualsOriginalMessage(secretMessage, resultDecryptedMessage) return resultDecryptedMessage diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt index 6095c58..16e87a3 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/KeyImportUseCase.kt @@ -130,7 +130,7 @@ class KeyImportUseCase @Inject constructor( // This will be the TEK key, which will be retained at the server side and which will be imported into the Android KeyStore. log.d("SERVER: Generating AES TEK for Android import ..") - serverStorage.tekAesKeyAtServer = server.generateAesTek(TEK_KEY_SIZE_BYTES) + serverStorage.tekAesKeyAtServer = server.generateAesTek(DomainConstants.TEK_KEY_SIZE_BITS) log.d("SERVER: Generating AES TEK for Android import - success") // Save the key locally for additional tests (the other button in the UI) @@ -138,14 +138,14 @@ class KeyImportUseCase @Inject constructor( // Server key generation phase 3. Server generates TEK key metadata, which will be used by StrongBox to import the TEK key log.d("SERVER: Generating TEK import metadata ..") - val tekImportMetadata = server.generateTekImportMetadata(TEK_KEY_SIZE_BYTES, KEY_MASTER_ALGORITHM_AES) + val tekImportMetadata = server.generateTekImportMetadata(DomainConstants.TEK_KEY_SIZE_BITS, KEY_MASTER_ALGORITHM_AES) log.d("SERVER: Metadata = $tekImportMetadata") log.d("SERVER: Generating TEK import metadata - success") // Server key generation phase 4. Server generates ephemeral AES secret key. // This will be CEK (Content Encryption Key), which will be temporarily used to wrap the TEK and additional metadata. log.d("SERVER: Generating AES CEK to wrap the TEK and Metadata ..") - val cekAesKeyAtServer = server.generateAesCek(CEK_KEY_SIZE_BYTES) + val cekAesKeyAtServer = server.generateAesCek(DomainConstants.CEK_KEY_SIZE_BITS) log.d("SERVER: Generating AES CEK to wrap the TEK and Metadata - success") // Server key generation phase 5. Server encrypts the CEK to the RSA public key with “RSA/ECB/OAEPPadding” encryption. @@ -209,8 +209,6 @@ class KeyImportUseCase @Inject constructor( } companion object { - private const val TEK_KEY_SIZE_BYTES = 256 - private const val CEK_KEY_SIZE_BYTES = 256 private const val KEY_MASTER_ALGORITHM_AES = 32 private const val BASE64_ENCODING_CHARSET_NAME = "UTF-8" } diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt index 569fad5..6dfa1ad 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoClient.kt @@ -6,7 +6,6 @@ import android.security.keystore.KeyInfo import android.security.keystore.KeyProperties import android.security.keystore.WrappedKeyEntry import androidx.annotation.RequiresApi -import com.nimbusds.jose.EncryptionMethod import com.nimbusds.jose.JWEAlgorithm import com.nimbusds.jose.JWEHeader import com.nimbusds.jose.JWEObject @@ -28,9 +27,7 @@ import java.security.spec.AlgorithmParameterSpec import java.security.spec.InvalidKeySpecException import javax.crypto.SecretKey import javax.crypto.SecretKeyFactory -import javax.crypto.spec.SecretKeySpec import javax.inject.Inject -import kotlin.random.Random @Suppress("EmptyClassBlock") class CryptoClient @Inject constructor() : CryptoClientGateway { @@ -41,8 +38,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { // Note: The KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT are here only for the extra tests, not needed for the import! val builder = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_WRAP_KEY or KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) - .setDigests(KeyProperties.DIGEST_SHA256) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) + .setDigests(KeyProperties.DIGEST_SHA256).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .setBlockModes(KeyProperties.BLOCK_MODE_ECB) if (isStrongBoxBacked) { @@ -118,8 +114,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { private fun isSecureKeyInsideSecureHardwareCompat(key: SecretKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: SecretKeyFactory = SecretKeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) - @Suppress("DEPRECATION") - return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { + @Suppress("DEPRECATION") return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } else { CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware @@ -128,8 +123,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { private fun isPrivateKeyInsideSecureHardwareCompat(key: PrivateKey): CryptoClientGateway.KeyTeeSecurityLevel { val factory: KeyFactory = KeyFactory.getInstance(key.algorithm, KEY_STORE_PROVIDER_ANDROID_KEYSTORE) - @Suppress("DEPRECATION") - return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { + @Suppress("DEPRECATION") return if ((factory.getKeySpec(key, KeyInfo::class.java) as KeyInfo).isInsideSecureHardware) { CryptoClientGateway.KeyTeeSecurityLevel.TeeHardwareNoStrongbox } else { CryptoClientGateway.KeyTeeSecurityLevel.TeeSoftware @@ -137,9 +131,7 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { } override fun encodeRsaPublicKeyAsJwk(alias: String, publicKey: PublicKey): String { - val jwk: JWK = RSAKey.Builder(publicKey as RSAPublicKey) - .keyID(alias) - .build() + val jwk: JWK = RSAKey.Builder(publicKey as RSAPublicKey).keyID(alias).build() return jwk.toJSONString() } @@ -147,9 +139,8 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { override fun importWrappedKeyFromServer(asn1DerEncodedWrappedKey: ByteArray, wrappingKeyAliasInKeysStore: String, wrappedKeyAlias: String) { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) - val spec: AlgorithmParameterSpec = KeyGenParameterSpec.Builder(wrappingKeyAliasInKeysStore, KeyProperties.PURPOSE_WRAP_KEY) - .setDigests(KeyProperties.DIGEST_SHA256) - .build() + val spec: AlgorithmParameterSpec = + KeyGenParameterSpec.Builder(wrappingKeyAliasInKeysStore, KeyProperties.PURPOSE_WRAP_KEY).setDigests(KeyProperties.DIGEST_SHA256).build() val wrappedKeyEntry = WrappedKeyEntry(asn1DerEncodedWrappedKey, wrappingKeyAliasInKeysStore, "RSA/ECB/OAEPPadding", spec) keyStore.setEntry(wrappedKeyAlias, wrappedKeyEntry, null) } @@ -168,12 +159,14 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { return jweObject.payload.toString() } - override fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String): String { + override fun encryptMessageWithTekToJWE(message: String, keyStoreKeyAlias: String, keySizeBits: Int): String { val keyStore: KeyStore = KeyStore.getInstance(KEY_STORE_PROVIDER_ANDROID_KEYSTORE) keyStore.load(null, null) val secretKeyEntry = keyStore.getEntry(keyStoreKeyAlias, null) as KeyStore.SecretKeyEntry - val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + val enc = CryptoUtil.getEncryptionMethodForKeySize(keySizeBits) + + val header = JWEHeader(JWEAlgorithm.DIR, enc) // Set the message as payload plain text val payload = Payload(message) @@ -188,14 +181,6 @@ class CryptoClient @Inject constructor() : CryptoClientGateway { return jweObject.serialize() } - @Suppress("MagicNumber", "unused") - private fun generateFakeDecryptionKey(@Suppress("SameParameterValue") keySize: Int): SecretKey { - val arraySize = keySize / 8 - val aesKeyBytes = ByteArray(arraySize) - Random.nextBytes(aesKeyBytes) - return SecretKeySpec(aesKeyBytes, "AES") - } - companion object { private const val KEY_STORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore" } diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt index 045cb61..15f3545 100644 --- a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoServer.kt @@ -1,6 +1,5 @@ package mobi.lab.keyimportdemo.infrastructure.crypto -import com.nimbusds.jose.EncryptionMethod import com.nimbusds.jose.JWEAlgorithm import com.nimbusds.jose.JWEHeader import com.nimbusds.jose.JWEObject @@ -9,6 +8,8 @@ import com.nimbusds.jose.crypto.DirectDecrypter import com.nimbusds.jose.crypto.DirectEncrypter import com.nimbusds.jose.jwk.JWK import mobi.lab.keyimportdemo.domain.gateway.CryptoServerGateway +import mobi.lab.keyimportdemo.infrastructure.crypto.CryptoUtil.toBitsFromBytes +import mobi.lab.keyimportdemo.infrastructure.crypto.CryptoUtil.toBytesFromBits import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.DERInteger @@ -35,16 +36,16 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { } @Suppress("MagicNumber") - override fun generateAesTek(keySize: Int): SecretKeySpec { + override fun generateAesTek(keySizeBits: Int): SecretKeySpec { // Generate keySize bit AES key for TEK - val arraySize = keySize / 8 - val aesKeyBytes = ByteArray(arraySize) + val keySizeBytes = keySizeBits.toBytesFromBits() + val aesKeyBytes = ByteArray(keySizeBytes) nextBytes(aesKeyBytes) return SecretKeySpec(aesKeyBytes, "AES") } @Suppress("MagicNumber") - private fun makeSecureKeyAuthorizationList(size: Int, algorithmInt: Int): DERSequence { + private fun makeSecureKeyAuthorizationList(keySizeBits: Int, algorithmInt: Int): DERSequence { // Make an AuthorizationList to describe the secure key // https://developer.android.com/training/articles/security-key-attestation.html#verifying val allPurposes = ASN1EncodableVector() @@ -53,7 +54,8 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { val purposeSet = DERSet(allPurposes) val purpose = DERTaggedObject(true, 1, purposeSet) val algorithm = DERTaggedObject(true, 2, DERInteger(algorithmInt)) - val keySize = DERTaggedObject(true, 3, DERInteger(size)) + // Size in bits https://source.android.com/docs/security/features/keystore/tags#key_size + val keySize = DERTaggedObject(true, 3, DERInteger(keySizeBits)) val allBlockModes = ASN1EncodableVector() allBlockModes.add(DERInteger(KM_MODE_ECB)) allBlockModes.add(DERInteger(KM_MODE_CBC)) @@ -76,8 +78,8 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { return DERSequence(allItems) } - override fun generateTekImportMetadata(keySizeBytes: Int, keyMasterAlgorithm: Int): DERSequence { - val authorizationList = makeSecureKeyAuthorizationList(keySizeBytes, keyMasterAlgorithm) + override fun generateTekImportMetadata(keySizeBits: Int, keyMasterAlgorithm: Int): DERSequence { + val authorizationList = makeSecureKeyAuthorizationList(keySizeBits, keyMasterAlgorithm) // Build description val descriptionItems = ASN1EncodableVector() descriptionItems.add(DERInteger(KM_KEY_FORMAT_RAW)) @@ -86,9 +88,9 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { } @Suppress("MagicNumber") - override fun generateAesCek(cekKeySizeBytes: Int): SecretKeySpec { - // Generate 256 bit AES key. This is the ephemeral key used to encrypt the secure key. - val aesKeyBytes = ByteArray(cekKeySizeBytes / 8) + override fun generateAesCek(cekKeySizeBits: Int): SecretKeySpec { + // Generate an AES key. This is the ephemeral key used to encrypt the secure key. + val aesKeyBytes = ByteArray(cekKeySizeBits.toBytesFromBits()) nextBytes(aesKeyBytes) return SecretKeySpec(aesKeyBytes, "AES") } @@ -112,17 +114,17 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { nextBytes(initializationVector) // Encrypt secure key val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val gcmParameterSpec = GCMParameterSpec(GCM_TAG_SIZE, initializationVector) + val gcmParameterSpec = GCMParameterSpec(GCM_TAG_SIZE_BITS, initializationVector) cipher.init(Cipher.ENCRYPT_MODE, cekAesKey, gcmParameterSpec) val aad: ByteArray = wrappedKeyDescription.encoded cipher.updateAAD(aad) var encryptedSecureKey = cipher.doFinal(tekAesWrappedKey.encoded) // Get GCM tag. Java puts the tag at the end of the ciphertext data :( val len = encryptedSecureKey.size - val tagSize: Int = GCM_TAG_SIZE / 8 - val tag: ByteArray = Arrays.copyOfRange(encryptedSecureKey, len - tagSize, len) + val tagSizeBytes: Int = GCM_TAG_SIZE_BITS.toBytesFromBits() + val tag: ByteArray = Arrays.copyOfRange(encryptedSecureKey, len - tagSizeBytes, len) // Remove GCM tag from end of output - encryptedSecureKey = Arrays.copyOfRange(encryptedSecureKey, 0, len - tagSize) + encryptedSecureKey = Arrays.copyOfRange(encryptedSecureKey, 0, len - tagSizeBytes) // Return both return CryptoServerGateway.EncryptedTekWrapper(encryptedSecureKey, tag, initializationVector) } @@ -147,7 +149,8 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { // Create the header // Uses GCM for now, // for more info see https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/490/jwe-with-shared-key-support-for-android - val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) + val enc = CryptoUtil.getEncryptionMethodForKeySize(tekAesKeyAtServer.encoded.size.toBitsFromBytes()) + val header = JWEHeader(JWEAlgorithm.DIR, enc) // Set the message as payload plain text val payload = Payload(message) @@ -177,7 +180,7 @@ class CryptoServer @Inject constructor() : CryptoServerGateway { private const val KM_PAD_NONE = 1 private const val KM_PAD_PKCS7 = 64 private const val KM_KEY_FORMAT_RAW = 3 - private const val GCM_TAG_SIZE = 128 + private const val GCM_TAG_SIZE_BITS = 128 private const val WRAPPED_FORMAT_VERSION = 0 } } diff --git a/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoUtil.kt b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoUtil.kt new file mode 100644 index 0000000..3541f61 --- /dev/null +++ b/app-infrastructure/src/main/java/mobi/lab/keyimportdemo/infrastructure/crypto/CryptoUtil.kt @@ -0,0 +1,34 @@ +package mobi.lab.keyimportdemo.infrastructure.crypto + +import com.nimbusds.jose.EncryptionMethod + +object CryptoUtil { + @Suppress("MagicNumber") + fun Int.toBitsFromBytes(): Int { + return this * 8 + } + + @Suppress("MagicNumber") + fun Int.toBytesFromBits(): Int { + return this / 8 + } + + @Suppress("MagicNumber") + fun getEncryptionMethodForKeySize(keySizeBits: Int): EncryptionMethod { + val enc = when (keySizeBits) { + 128 -> { + EncryptionMethod.A128GCM + } + 192 -> { + EncryptionMethod.A192GCM + } + 256 -> { + EncryptionMethod.A256GCM + } + else -> { + throw IllegalArgumentException("Unsupported key size $keySizeBits!") + } + } + return enc + } +} From 452b0bc50ebd8407e53885dfba42c3538e5549cb Mon Sep 17 00:00:00 2001 From: Harri Kirik Date: Wed, 28 Sep 2022 09:42:33 +0300 Subject: [PATCH 7/7] 1: Use the correct key type verification --- .../domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt index 7fc8743..a4cf7aa 100644 --- a/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt +++ b/app-domain/src/main/java/mobi/lab/keyimportdemo/domain/usecases/crypto/ImportedKeyTwoWayUsageUseCase.kt @@ -39,7 +39,7 @@ class ImportedKeyTwoWayUsageUseCase @Inject constructor( log.d("Testing client -> server message flow test finished") KeyUsageTestResult.UsageSuccess( - client.getPrivateKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS), + client.getSecretKeySecurityLevel(DomainConstants.DEVICE_TEE_IMPORT_KEY_ALIAS), serverToClientSecretMessageResult, clientToServerSecretMessageResult )