ganfra
11 months ago
committed by
GitHub
45 changed files with 963 additions and 59 deletions
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin |
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore |
||||
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService |
||||
import io.element.android.libraries.cryptography.api.EncryptionResult |
||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider |
||||
import io.element.android.libraries.di.AppScope |
||||
import javax.inject.Inject |
||||
|
||||
private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE" |
||||
|
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultPinCodeManager @Inject constructor( |
||||
private val secretKeyProvider: SecretKeyProvider, |
||||
private val encryptionDecryptionService: EncryptionDecryptionService, |
||||
private val pinCodeStore: PinCodeStore, |
||||
) : PinCodeManager { |
||||
|
||||
override suspend fun isPinCodeAvailable(): Boolean { |
||||
return pinCodeStore.hasPinCode() |
||||
} |
||||
|
||||
override suspend fun createPinCode(pinCode: String) { |
||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) |
||||
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() |
||||
pinCodeStore.saveEncryptedPinCode(encryptedPinCode) |
||||
} |
||||
|
||||
override suspend fun verifyPinCode(pinCode: String): Boolean { |
||||
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return false |
||||
return try { |
||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) |
||||
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) |
||||
decryptedPinCode.contentEquals(pinCode.toByteArray()) |
||||
} catch (failure: Throwable) { |
||||
false |
||||
} |
||||
} |
||||
|
||||
override suspend fun deletePinCode() { |
||||
pinCodeStore.deleteEncryptedPinCode() |
||||
} |
||||
|
||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int { |
||||
return pinCodeStore.getRemainingPinCodeAttemptsNumber() |
||||
} |
||||
|
||||
override suspend fun onWrongPin(): Int { |
||||
return pinCodeStore.onWrongPin() |
||||
} |
||||
|
||||
override suspend fun resetCounter() { |
||||
pinCodeStore.resetCounter() |
||||
} |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin |
||||
|
||||
/** |
||||
* This interface is the main interface to manage the pin code. |
||||
* Implementation should take care of encrypting the pin code and storing it. |
||||
*/ |
||||
interface PinCodeManager { |
||||
/** |
||||
* @return true if a pin code is available. |
||||
*/ |
||||
suspend fun isPinCodeAvailable(): Boolean |
||||
|
||||
/** |
||||
* Creates a new encrypted pin code. |
||||
* @param pinCode the clear pin code to create |
||||
*/ |
||||
suspend fun createPinCode(pinCode: String) |
||||
|
||||
/** |
||||
* @return true if the pin code is correct. |
||||
*/ |
||||
suspend fun verifyPinCode(pinCode: String): Boolean |
||||
|
||||
/** |
||||
* Deletes the previously created pin code. |
||||
*/ |
||||
suspend fun deletePinCode() |
||||
|
||||
/** |
||||
* @return the number of remaining attempts before the pin code is blocked. |
||||
*/ |
||||
suspend fun getRemainingPinCodeAttemptsNumber(): Int |
||||
|
||||
/** |
||||
* Should be called when the pin code is incorrect. |
||||
* Will decrement the remaining attempts number. |
||||
* @return the number of remaining attempts before the pin code is blocked. |
||||
*/ |
||||
suspend fun onWrongPin(): Int |
||||
|
||||
/** |
||||
* Resets the counter of attempts for PIN code. |
||||
*/ |
||||
suspend fun resetCounter() |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin.storage |
||||
|
||||
/** |
||||
* Should be implemented by any class that provides access to the encrypted PIN code. |
||||
* All methods are suspending in case there are async IO operations involved. |
||||
*/ |
||||
interface EncryptedPinCodeStorage { |
||||
/** |
||||
* Returns the encrypted PIN code. |
||||
*/ |
||||
suspend fun getEncryptedCode(): String? |
||||
|
||||
/** |
||||
* Saves the encrypted PIN code to some persistable storage. |
||||
*/ |
||||
suspend fun saveEncryptedPinCode(pinCode: String) |
||||
|
||||
/** |
||||
* Deletes the PIN code from some persistable storage. |
||||
*/ |
||||
suspend fun deleteEncryptedPinCode() |
||||
|
||||
/** |
||||
* Returns whether the PIN code is stored or not. |
||||
*/ |
||||
suspend fun hasPinCode(): Boolean |
||||
} |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin.storage |
||||
|
||||
interface PinCodeStore : EncryptedPinCodeStorage { |
||||
|
||||
interface Listener { |
||||
fun onPinSetUpChange(isConfigured: Boolean) |
||||
} |
||||
|
||||
/** |
||||
* Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time. |
||||
*/ |
||||
suspend fun getRemainingPinCodeAttemptsNumber(): Int |
||||
|
||||
/** |
||||
* Should decrement the number of remaining PIN code attempts. |
||||
* @return The remaining attempts. |
||||
*/ |
||||
suspend fun onWrongPin(): Int |
||||
|
||||
/** |
||||
* Resets the counter of attempts for PIN code and biometric access. |
||||
*/ |
||||
suspend fun resetCounter() |
||||
|
||||
/** |
||||
* Adds a listener to be notified when the PIN code us created or removed. |
||||
*/ |
||||
fun addListener(listener: Listener) |
||||
|
||||
/** |
||||
* Removes a listener to be notified when the PIN code us created or removed. |
||||
*/ |
||||
fun removeListener(listener: Listener) |
||||
} |
||||
|
||||
|
@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin.storage |
||||
|
||||
import android.content.SharedPreferences |
||||
import androidx.core.content.edit |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.di.SingleIn |
||||
import kotlinx.coroutines.sync.Mutex |
||||
import kotlinx.coroutines.sync.withLock |
||||
import kotlinx.coroutines.withContext |
||||
import java.util.concurrent.CopyOnWriteArrayList |
||||
import javax.inject.Inject |
||||
|
||||
private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" |
||||
private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" |
||||
private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 |
||||
|
||||
@SingleIn(AppScope::class) |
||||
@ContributesBinding(AppScope::class) |
||||
class SharedPreferencesPinCodeStore @Inject constructor( |
||||
private val dispatchers: CoroutineDispatchers, |
||||
private val sharedPreferences: SharedPreferences, |
||||
) : PinCodeStore { |
||||
|
||||
private val listeners = CopyOnWriteArrayList<PinCodeStore.Listener>() |
||||
private val mutex = Mutex() |
||||
|
||||
override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { |
||||
sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) |
||||
} |
||||
|
||||
override suspend fun saveEncryptedPinCode(pinCode: String) = withContext(dispatchers.io) { |
||||
sharedPreferences.edit { |
||||
putString(ENCODED_PIN_CODE_KEY, pinCode) |
||||
} |
||||
withContext(dispatchers.main) { |
||||
listeners.forEach { it.onPinSetUpChange(isConfigured = true) } |
||||
} |
||||
} |
||||
|
||||
override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { |
||||
// Also reset the counters |
||||
resetCounter() |
||||
sharedPreferences.edit { |
||||
remove(ENCODED_PIN_CODE_KEY) |
||||
} |
||||
withContext(dispatchers.main) { |
||||
listeners.forEach { it.onPinSetUpChange(isConfigured = false) } |
||||
} |
||||
} |
||||
|
||||
override suspend fun hasPinCode(): Boolean = withContext(dispatchers.io) { |
||||
sharedPreferences.contains(ENCODED_PIN_CODE_KEY) |
||||
} |
||||
|
||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { |
||||
mutex.withLock { |
||||
sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) |
||||
} |
||||
} |
||||
|
||||
override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { |
||||
mutex.withLock { |
||||
val remaining = getRemainingPinCodeAttemptsNumber() - 1 |
||||
sharedPreferences.edit { |
||||
putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) |
||||
} |
||||
remaining |
||||
} |
||||
} |
||||
|
||||
override suspend fun resetCounter() = withContext(dispatchers.io) { |
||||
mutex.withLock { |
||||
sharedPreferences.edit { |
||||
remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun addListener(listener: PinCodeStore.Listener) { |
||||
listeners.add(listener) |
||||
} |
||||
|
||||
override fun removeListener(listener: PinCodeStore.Listener) { |
||||
listeners.remove(listener) |
||||
} |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin |
||||
|
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore |
||||
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService |
||||
import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class DefaultPinCodeManagerTest { |
||||
|
||||
private val pinCodeStore = InMemoryPinCodeStore() |
||||
private val secretKeyProvider = SimpleSecretKeyProvider() |
||||
private val encryptionDecryptionService = AESEncryptionDecryptionService() |
||||
private val pinCodeManager = DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) |
||||
|
||||
@Test |
||||
fun `given a pin code when create and delete assert no pin code left`() = runTest { |
||||
pinCodeManager.createPinCode("1234") |
||||
assertThat(pinCodeManager.isPinCodeAvailable()).isTrue() |
||||
pinCodeManager.deletePinCode() |
||||
assertThat(pinCodeManager.isPinCodeAvailable()).isFalse() |
||||
} |
||||
|
||||
@Test |
||||
fun `given a pin code when create and verify with the same pin succeed`() = runTest { |
||||
val pinCode = "1234" |
||||
pinCodeManager.createPinCode(pinCode) |
||||
assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue() |
||||
} |
||||
|
||||
@Test |
||||
fun `given a pin code when create and verify with a different pin fails`() = runTest { |
||||
pinCodeManager.createPinCode("1234") |
||||
assertThat(pinCodeManager.verifyPinCode("1235")).isFalse() |
||||
} |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.lockscreen.impl.pin.storage |
||||
|
||||
private const val DEFAULT_REMAINING_ATTEMPTS = 3 |
||||
|
||||
class InMemoryPinCodeStore : PinCodeStore { |
||||
|
||||
private var pinCode: String? = null |
||||
private var remainingAttempts: Int = DEFAULT_REMAINING_ATTEMPTS |
||||
|
||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int { |
||||
return remainingAttempts |
||||
} |
||||
|
||||
override suspend fun onWrongPin(): Int { |
||||
return remainingAttempts-- |
||||
} |
||||
|
||||
override suspend fun resetCounter() { |
||||
remainingAttempts = DEFAULT_REMAINING_ATTEMPTS |
||||
} |
||||
|
||||
override fun addListener(listener: PinCodeStore.Listener) { |
||||
// no-op |
||||
} |
||||
|
||||
override fun removeListener(listener: PinCodeStore.Listener) { |
||||
// no-op |
||||
} |
||||
|
||||
override suspend fun getEncryptedCode(): String? { |
||||
return pinCode |
||||
} |
||||
|
||||
override suspend fun saveEncryptedPinCode(pinCode: String) { |
||||
this.pinCode = pinCode |
||||
} |
||||
|
||||
override suspend fun deleteEncryptedPinCode() { |
||||
pinCode = null |
||||
} |
||||
|
||||
override suspend fun hasPinCode(): Boolean { |
||||
return pinCode != null |
||||
} |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-library") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.libraries.cryptography.api" |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.api |
||||
|
||||
import android.security.keystore.KeyProperties |
||||
|
||||
object AESEncryptionSpecs { |
||||
const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM |
||||
const val PADDINGS = KeyProperties.ENCRYPTION_PADDING_NONE |
||||
const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES |
||||
const val KEY_SIZE = 128 |
||||
const val CIPHER_TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDINGS" |
||||
} |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.api |
||||
|
||||
import javax.crypto.Cipher |
||||
import javax.crypto.SecretKey |
||||
|
||||
/** |
||||
* Simple service to provide encryption and decryption operations. |
||||
*/ |
||||
interface EncryptionDecryptionService { |
||||
fun createEncryptionCipher(key: SecretKey): Cipher |
||||
fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher |
||||
fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult |
||||
fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray |
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
@file:OptIn(ExperimentalEncodingApi::class) |
||||
|
||||
package io.element.android.libraries.cryptography.api |
||||
|
||||
import java.nio.ByteBuffer |
||||
import kotlin.io.encoding.Base64 |
||||
import kotlin.io.encoding.ExperimentalEncodingApi |
||||
|
||||
/** |
||||
* Holds the result of an encryption operation. |
||||
*/ |
||||
class EncryptionResult( |
||||
val encryptedByteArray: ByteArray, |
||||
val initializationVector: ByteArray |
||||
) { |
||||
fun toBase64(): String { |
||||
val initializationVectorSize = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(initializationVector.size).array() |
||||
val cipherTextWithIv: ByteArray = |
||||
ByteBuffer.allocate(Int.SIZE_BYTES + initializationVector.size + encryptedByteArray.size) |
||||
.put(initializationVectorSize) |
||||
.put(initializationVector) |
||||
.put(encryptedByteArray) |
||||
.array() |
||||
return Base64.encode(cipherTextWithIv) |
||||
} |
||||
|
||||
companion object { |
||||
/** |
||||
* @param base64 the base64 representation of the encrypted data. |
||||
* @return the [EncryptionResult] from the base64 representation. |
||||
*/ |
||||
fun fromBase64(base64: String): EncryptionResult { |
||||
val cipherTextWithIv = Base64.decode(base64) |
||||
val buffer = ByteBuffer.wrap(cipherTextWithIv) |
||||
val initializationVectorSize = buffer.int |
||||
val initializationVector = ByteArray(initializationVectorSize) |
||||
buffer.get(initializationVector) |
||||
val encryptedByteArray = ByteArray(buffer.remaining()) |
||||
buffer.get(encryptedByteArray) |
||||
return EncryptionResult(encryptedByteArray, initializationVector) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.api |
||||
|
||||
import javax.crypto.SecretKey |
||||
|
||||
/** |
||||
* Simple interface to get or create a secret key for a given alias. |
||||
* Implementation should be able to store the generated key securely. |
||||
*/ |
||||
interface SecretKeyProvider { |
||||
fun getOrCreateKey(alias: String): SecretKey |
||||
} |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-library") |
||||
alias(libs.plugins.anvil) |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.libraries.cryptography.impl" |
||||
} |
||||
|
||||
anvil { |
||||
generateDaggerFactories.set(true) |
||||
} |
||||
|
||||
dependencies { |
||||
anvil(projects.anvilcodegen) |
||||
implementation(libs.dagger) |
||||
implementation(projects.anvilannotations) |
||||
implementation(projects.libraries.di) |
||||
implementation(projects.libraries.cryptography.api) |
||||
|
||||
testImplementation(libs.test.junit) |
||||
testImplementation(libs.test.truth) |
||||
} |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.impl |
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs |
||||
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService |
||||
import io.element.android.libraries.cryptography.api.EncryptionResult |
||||
import io.element.android.libraries.di.AppScope |
||||
import javax.crypto.Cipher |
||||
import javax.crypto.SecretKey |
||||
import javax.crypto.spec.GCMParameterSpec |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Default implementation of [EncryptionDecryptionService] using AES encryption. |
||||
*/ |
||||
@ContributesBinding(AppScope::class) |
||||
class AESEncryptionDecryptionService @Inject constructor() : EncryptionDecryptionService { |
||||
|
||||
override fun createEncryptionCipher(key: SecretKey): Cipher { |
||||
return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { |
||||
init(Cipher.ENCRYPT_MODE, key) |
||||
} |
||||
} |
||||
|
||||
override fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher { |
||||
val spec = GCMParameterSpec(128, initializationVector) |
||||
return Cipher.getInstance(AESEncryptionSpecs.CIPHER_TRANSFORMATION).apply { |
||||
init(Cipher.DECRYPT_MODE, key, spec) |
||||
} |
||||
} |
||||
|
||||
override fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult { |
||||
val cipher = createEncryptionCipher(key) |
||||
val encryptedData = cipher.doFinal(input) |
||||
return EncryptionResult(encryptedData, cipher.iv) |
||||
} |
||||
|
||||
override fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray { |
||||
val cipher = createDecryptionCipher(key, encryptionResult.initializationVector) |
||||
return cipher.doFinal(encryptionResult.encryptedByteArray) |
||||
} |
||||
} |
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.impl |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.security.keystore.KeyGenParameterSpec |
||||
import android.security.keystore.KeyProperties |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs |
||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider |
||||
import io.element.android.libraries.di.AppScope |
||||
import java.security.KeyStore |
||||
import javax.crypto.KeyGenerator |
||||
import javax.crypto.SecretKey |
||||
import javax.inject.Inject |
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore" |
||||
|
||||
/** |
||||
* Default implementation of [SecretKeyProvider] that uses the Android Keystore to store the keys. |
||||
* The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. |
||||
*/ |
||||
@ContributesBinding(AppScope::class) |
||||
class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { |
||||
|
||||
// False positive lint issue |
||||
@SuppressLint("WrongConstant") |
||||
override fun getOrCreateKey(alias: String): SecretKey { |
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) |
||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) |
||||
?.secretKey |
||||
return if (secretKeyEntry == null) { |
||||
val generator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM, ANDROID_KEYSTORE) |
||||
val keyGenSpec = KeyGenParameterSpec.Builder( |
||||
alias, |
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT |
||||
) |
||||
.setBlockModes(AESEncryptionSpecs.BLOCK_MODE) |
||||
.setEncryptionPaddings(AESEncryptionSpecs.PADDINGS) |
||||
.setKeySize(AESEncryptionSpecs.KEY_SIZE) |
||||
.build() |
||||
generator.init(keyGenSpec) |
||||
generator.generateKey() |
||||
} else { |
||||
secretKeyEntry |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.impl |
||||
|
||||
import android.security.keystore.KeyProperties |
||||
import com.google.common.truth.Truth.assertThat |
||||
import org.junit.Assert.assertThrows |
||||
import org.junit.Test |
||||
import java.security.GeneralSecurityException |
||||
import javax.crypto.KeyGenerator |
||||
|
||||
class AESEncryptionDecryptionServiceTest { |
||||
|
||||
private val encryptionDecryptionService = AESEncryptionDecryptionService() |
||||
|
||||
@Test |
||||
fun `given a valid key then encrypt decrypt work`() { |
||||
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) |
||||
keyGenerator.init(128) |
||||
val key = keyGenerator.generateKey() |
||||
val input = "Hello World".toByteArray() |
||||
val encryptionResult = encryptionDecryptionService.encrypt(key, input) |
||||
val decrypted = encryptionDecryptionService.decrypt(key, encryptionResult) |
||||
assertThat(decrypted).isEqualTo(input) |
||||
} |
||||
|
||||
@Test |
||||
fun `given a wrong key then decrypt fail`() { |
||||
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) |
||||
keyGenerator.init(128) |
||||
val encryptionKey = keyGenerator.generateKey() |
||||
val input = "Hello World".toByteArray() |
||||
val encryptionResult = encryptionDecryptionService.encrypt(encryptionKey, input) |
||||
val decryptionKey = keyGenerator.generateKey() |
||||
assertThrows(GeneralSecurityException::class.java) { |
||||
encryptionDecryptionService.decrypt(decryptionKey, encryptionResult) |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id("io.element.android-library") |
||||
} |
||||
|
||||
android { |
||||
namespace = "io.element.android.libraries.cryptography.test" |
||||
|
||||
dependencies { |
||||
api(projects.libraries.cryptography.api) |
||||
} |
||||
} |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/* |
||||
* Copyright (c) 2023 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.cryptography.test |
||||
|
||||
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs |
||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider |
||||
import javax.crypto.KeyGenerator |
||||
import javax.crypto.SecretKey |
||||
|
||||
class SimpleSecretKeyProvider : SecretKeyProvider { |
||||
|
||||
private var secretKeyForAlias = HashMap<String, SecretKey>() |
||||
|
||||
override fun getOrCreateKey(alias: String): SecretKey { |
||||
return secretKeyForAlias.getOrPut(alias) { |
||||
generateKey() |
||||
} |
||||
} |
||||
|
||||
private fun generateKey(): SecretKey { |
||||
val keyGenerator = KeyGenerator.getInstance(AESEncryptionSpecs.ALGORITHM) |
||||
keyGenerator.init(AESEncryptionSpecs.KEY_SIZE) |
||||
return keyGenerator.generateKey() |
||||
} |
||||
} |
Loading…
Reference in new issue