Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sang/security audit/revert biometric #15

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ dependencies {
testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
api "androidx.biometric:biometric:1.1.0"

}
1 change: 0 additions & 1 deletion libauk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ android {

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
api "androidx.biometric:biometric:1.1.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ class SecureFileStorageTest {
secureFileStorage.rxCompletable { gw ->
gw.writeOnFilesDir(
d.key,
d.value.toByteArray(),
true
d.value.toByteArray()
)
}
.test()
.assertComplete()
.assertNoErrors()

secureFileStorage.readOnFilesDir(d.key).map { byteArray -> String(byteArray) }
secureFileStorage.rxSingle { gw -> String(gw.readOnFilesDir(d.key)) }
.test()
.assertComplete()
.assertNoErrors()
Expand All @@ -65,15 +64,14 @@ class SecureFileStorageTest {
secureFileStorage.rxCompletable { gw ->
gw.writeOnFilesDir(
d.key,
d.value.toByteArray(),
true
d.value.toByteArray()
)
}
.test()
.assertComplete()
.assertNoErrors()

secureFileStorage.readOnFilesDir(d.key).map { byteArray -> String(byteArray) }
secureFileStorage.rxSingle { gw -> String(gw.readOnFilesDir(d.key)) }
.test()
.assertComplete()
.assertNoErrors()
Expand Down
84 changes: 9 additions & 75 deletions libauk/src/main/java/com/bitmark/libauk/model/Keys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,92 +3,26 @@ package com.bitmark.libauk.model
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName
import org.web3j.crypto.Bip32ECKeyPair
import java.math.BigInteger
import java.util.*

@JsonSerialize
data class KeyInfo(
data class KeyIdentity(
@Expose
@SerializedName("ethAddress")
val ethAddress: String,
@SerializedName("words")
val words: String,

@Expose
@SerializedName("creationDate")
val creationDate: Date
@SerializedName("passphrase")
val passphrase: String
)

@JsonSerialize
data class SeedPublicData(
data class KeyInfo(
@Expose
@SerializedName("ethAddress")
val ethAddress: String,

@Expose
@SerializedName("creationDate")
val creationDate: Date,

@Expose
@SerializedName("name")
val name: String?,

@Expose
@SerializedName("did")
val did: String,

@Expose
@SerializedName("preGenerateEthAddresses")
val preGenerateEthAddresses: Map<Int, String>,

@Expose
@SerializedName("preGenerateTezosAddresses")
val preGenerateTezosAddresses: Map<Int, String>,

@Expose
@SerializedName("preGenerateTezosPublicKeys")
val preGenerateTezosPublicKeys: Map<Int, String>,

@Expose
@SerializedName("encryptionPrivateKey")
val encryptionPrivateKey: ByteArray,

@Expose
@SerializedName("dIDPrivateKey")
private var dIDPrivateKey: BigInteger,

@Expose
@SerializedName("chainCode")
private val chainCode: ByteArray

) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as SeedPublicData

if (ethAddress != other.ethAddress) return false
if (creationDate != other.creationDate) return false
if (name != other.name) return false
if (did != other.did) return false
if (preGenerateEthAddresses != other.preGenerateEthAddresses) return false
if (preGenerateTezosAddresses != other.preGenerateTezosAddresses) return false
if (!encryptionPrivateKey.contentEquals(other.encryptionPrivateKey)) return false

return true
}

override fun hashCode(): Int {
var result = ethAddress.hashCode()
result = 31 * result + creationDate.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + did.hashCode()
result = 31 * result + preGenerateEthAddresses.hashCode()
result = 31 * result + preGenerateTezosAddresses.hashCode()
result = 31 * result + encryptionPrivateKey.contentHashCode()
return result
}

fun getAccountDIDPrivateKey(): Bip32ECKeyPair {
return Bip32ECKeyPair.create(dIDPrivateKey, chainCode)
}
}
val creationDate: Date
)
109 changes: 26 additions & 83 deletions libauk/src/main/java/com/bitmark/libauk/storage/SecureFileStorage.kt
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
package com.bitmark.libauk.storage

import com.bitmark.libauk.util.BiometricUtil
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.fragment.app.FragmentActivity
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKey
import io.reactivex.Completable
import io.reactivex.Single
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.KeyStore
import java.util.UUID
import java.util.*

internal interface SecureFileStorage {

fun writeOnFilesDir(name: String, data: ByteArray, isPrivate: Boolean)
fun writeOnFilesDir(name: String, data: ByteArray)

fun readOnFilesDir(name: String): Single<ByteArray>

fun readOnFilesDirWithoutAuthentication(name: String): ByteArray
fun readOnFilesDir(name: String): ByteArray

fun isExistingOnFilesDir(name: String): Boolean

fun deleteOnFilesDir(name: String): Boolean

fun readFiles(names: List<String>): Single<Map<String, ByteArray>>
}

internal class SecureFileStorageImpl(
Expand All @@ -43,22 +37,21 @@ internal class SecureFileStorageImpl(
value?.let { sharedPreferences.edit().putString(KEY_MASTER_KEY_ALIAS, it).apply() }
}

private fun write(path: String, name: String, data: ByteArray, isPrivate: Boolean) {
val file = getEncryptedFile("$path/$name", false, isPrivate)
private fun write(path: String, name: String, data: ByteArray) {
val file = getEncryptedFile("$path/$name", false)
file.openFileOutput().apply {
write(data)
flush()
close()
}
}

override fun writeOnFilesDir(name: String, data: ByteArray, isPrivate: Boolean) {
write(context.filesDir.absolutePath, "$alias-$name", data, isPrivate)
override fun writeOnFilesDir(name: String, data: ByteArray) {
write(context.filesDir.absolutePath, "$alias-$name", data)
}

private fun read(path: String, isPrivate: Boolean): ByteArray {

val file = getEncryptedFile(path, true, isPrivate)
private fun read(path: String): ByteArray {
val file = getEncryptedFile(path, true)
if (File(path).length() == 0L) return byteArrayOf()
val inputStream = file.openFileInput()
val os = ByteArrayOutputStream()
Expand All @@ -70,54 +63,8 @@ internal class SecureFileStorageImpl(
return os.toByteArray()
}

override fun readOnFilesDir(name: String): Single<ByteArray> {
val isAuthenRequired = BiometricUtil.isAuthenReuired(listOf(name), context)
return if (isAuthenRequired) {
if (context is FragmentActivity) {
return BiometricUtil.withAuthenticate<ByteArray>(activity = context,
onAuthenticationSucceeded = { result ->
read(
File(context.filesDir, "$alias-$name").absolutePath,
isAuthenRequired
)
},
onAuthenticationError = { _, _ -> byteArrayOf() },
onAuthenticationFailed = { byteArrayOf() }
)
} else {
Single.error(IllegalStateException("Context is not an instance of FragmentActivity"))
}
} else {
return Single.fromCallable { read(File(context.filesDir, "$alias-$name").absolutePath, isAuthenRequired) }
}
}

override fun readOnFilesDirWithoutAuthentication(name: String): ByteArray {
return read(File(context.filesDir, "$alias-$name").absolutePath, false)
}

override fun readFiles(names: List<String>): Single<Map<String, ByteArray>> {
val isAuthenRequired = BiometricUtil.isAuthenReuired(names, context)

fun readFileContents(): Map<String, ByteArray> = names.associateWith { name ->
read(File(context.filesDir, "$alias-$name").absolutePath, isAuthenRequired)
}

return if (isAuthenRequired) {
if (context is FragmentActivity) {
BiometricUtil.withAuthenticate<Map<String, ByteArray>>(
activity = context,
onAuthenticationSucceeded = { readFileContents() },
onAuthenticationError = { _, _ -> emptyMap() },
onAuthenticationFailed = { emptyMap() }
)
} else {
Single.error(IllegalStateException("Context is not an instance of FragmentActivity"))
}
} else {
Single.fromCallable { readFileContents() }
}
}
override fun readOnFilesDir(name: String): ByteArray =
read(File(context.filesDir, "$alias-$name").absolutePath)

private fun isExisting(path: String): Boolean = File(path).exists()

Expand All @@ -136,48 +83,44 @@ internal class SecureFileStorageImpl(
override fun deleteOnFilesDir(name: String): Boolean =
delete(File(context.filesDir, "$alias-$name").absolutePath)

private fun getEncryptedFile(path: String, read: Boolean, isPrivate: Boolean) = File(path).let { f ->
private fun getEncryptedFile(path: String, read: Boolean) = File(path).let { f ->
if (f.isDirectory) throw IllegalArgumentException("do not support directory")
if (read && !f.exists() && !f.createNewFile()) {
throw IllegalStateException("cannot create new file for reading")
} else if (!read && f.exists() && !f.delete()) {
throw IllegalStateException("cannot delete file before writing")
}
getEncryptedFileBuilder(f, isPrivate).build()
getEncryptedFileBuilder(f).build()
}

private fun getEncryptedFileBuilder(f: File, isPrivate: Boolean) = EncryptedFile.Builder(
private fun getEncryptedFileBuilder(f: File) = EncryptedFile.Builder(
context,
f,
getMasterKey(isPrivate),
getMasterKey(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
)

private fun getMasterKey(isPrivate: Boolean): MasterKey {
private fun getMasterKey(): MasterKey {
keyStore.load(null)

val keyAlias = masterKeyAlias ?: UUID.randomUUID().toString().also { masterKeyAlias = it }
val authenticationTimeoutInSeconds = 5
val parameterSpecBuilder = KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).apply {

val parameterSpec = KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setKeySize(256)
setDigests(KeyProperties.DIGEST_SHA512)
setUserAuthenticationRequired(false)
setUnlockedDeviceRequired(true)
setIsStrongBoxBacked(true)
setRandomizedEncryptionRequired(true)
setInvalidatedByBiometricEnrollment(true)
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
}
// if android version is higher than 28
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
parameterSpecBuilder.apply {
setUnlockedDeviceRequired(true)
setIsStrongBoxBacked(true)
}
}

val parameterSpec = parameterSpecBuilder.build()
}.build()

return MasterKey.Builder(context, keyAlias)
.setKeyGenParameterSpec(parameterSpec)
.setUserAuthenticationRequired(isPrivate, authenticationTimeoutInSeconds)
.build()
}

Expand Down
Loading