Merge pull request #1592 from vector-im/feature/fga/setup_crypto_for_pin

Feature/fga/setup crypto for pin
This commit is contained in:
ganfra 2023-10-18 17:21:47 +02:00 committed by GitHub
commit 00e885fa9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 964 additions and 60 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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