ganfra
11 months ago
14 changed files with 371 additions and 45 deletions
@ -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,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. |
||||
*/ |
||||
|
||||
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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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