ganfra
11 months ago
14 changed files with 371 additions and 45 deletions
@ -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,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. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.lockscreen.impl.pin.storage |
||||||
|
|
||||||
|
class InMemoryPinCodeStore : PinCodeStore { |
||||||
|
|
||||||
|
private var pinCode: String? = null |
||||||
|
private var remainingAttempts: Int = 3 |
||||||
|
|
||||||
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int { |
||||||
|
return remainingAttempts |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun onWrongPin(): Int { |
||||||
|
return remainingAttempts-- |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun resetCounter() { |
||||||
|
remainingAttempts = 3 |
||||||
|
} |
||||||
|
|
||||||
|
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,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,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,55 @@ |
|||||||
|
/* |
||||||
|
* 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.KeyGenParameterSpec |
||||||
|
import android.security.keystore.KeyProperties |
||||||
|
import io.element.android.libraries.cryptography.api.AESEncryptionSpecs |
||||||
|
import io.element.android.libraries.cryptography.api.SecretKeyProvider |
||||||
|
import java.security.KeyStore |
||||||
|
import javax.crypto.KeyGenerator |
||||||
|
import javax.crypto.SecretKey |
||||||
|
|
||||||
|
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. |
||||||
|
*/ |
||||||
|
class KeyStoreSecretKeyProvider : SecretKeyProvider { |
||||||
|
|
||||||
|
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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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