Skip to content

Commit

Permalink
Merge branch 'develop' into 'master'
Browse files Browse the repository at this point in the history
Develop

See merge request papers/airgap/airgap-vault!237
  • Loading branch information
godenzim committed Feb 26, 2021
2 parents 5767fd3 + 8c23038 commit d6a4c99
Show file tree
Hide file tree
Showing 14 changed files with 5,695 additions and 2,130 deletions.
10 changes: 10 additions & 0 deletions android/app/src/main/java/it/airgap/vault/plugin/PluginError.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package it.airgap.vault.plugin

data class CodedError(val message: String, val code: Int) {
override fun toString(): String = "$message (code: $code)"
}

object PluginError {
val maxAuthenticationRetriesVault = CodedError("Max authentication tries exceeded", 1)
val maxAuthenticationRetriesSystem = CodedError("Max authentication tries exceeded", 2)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package it.airgap.vault.plugin.securityutils

enum class AuthOrigin {
VAULT, SYSTEM
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.scottyab.rootbeer.RootBeer
import it.airgap.vault.BuildConfig
import it.airgap.vault.plugin.PluginError
import it.airgap.vault.plugin.securityutils.SecurityUtils.Companion.REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS
import it.airgap.vault.plugin.securityutils.storage.Storage
import it.airgap.vault.util.assertReceived
import it.airgap.vault.util.freeCallIfSaved
import it.airgap.vault.util.logDebug
import it.airgap.vault.util.resolveWithData
import java.util.*
Expand Down Expand Up @@ -52,6 +54,10 @@ class SecurityUtils : Plugin() {
return !isAuthenticated
}

private var authTries: Int = 0
@Synchronized get
@Synchronized set

private val integrityAssessment: Boolean
get() {
val isRooted = RootBeer(context).isRootedWithoutBusyBoxCheck
Expand All @@ -66,15 +72,18 @@ class SecurityUtils : Plugin() {

@PluginMethod
fun initStorage(call: PluginCall) {
saveCall(call)
with (call) {
try {
assertReceived(Param.ALIAS, Param.IS_PARANOIA)
if (isParanoia) {
setupParanoiaPassword(call)
} else {
freeCallIfSaved()
resolve()
}
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
Expand All @@ -96,130 +105,158 @@ class SecurityUtils : Plugin() {

@PluginMethod
fun getItem(call: PluginCall) {
saveCall(call)
with (call) {
try {
assessIntegrity()
assertReceived(Param.ALIAS, Param.IS_PARANOIA, Param.FILE_KEY)

Storage(context, alias, isParanoia).readString(key, {
logDebug("getItem: success")
freeCallIfSaved()
resolveWithData(Key.VALUE to it)
}, {
logDebug("getItem: failure")
freeCallIfSaved()
reject(it.toString())
}, { showAuthenticationScreen(call, it) })
}, createAuthenticationRequestedHandler(call))
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun setItem(call: PluginCall) {
saveCall(call)
with (call) {
try {
assessIntegrity()
assertReceived(Param.ALIAS, Param.IS_PARANOIA, Param.FILE_KEY, Param.VALUE)

Storage(context, alias, isParanoia).writeString(key, value, {
logDebug("setItem: success")
freeCallIfSaved()
resolve()
}, {
logDebug("setItem: failure")
freeCallIfSaved()
reject(it.toString())
}, { showAuthenticationScreen(call, it) })
}, createAuthenticationRequestedHandler(call))
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun removeAll(call: PluginCall) {
saveCall(call)
with (call) {
try {
assertReceived(Param.ALIAS)

val result = Storage.removeAll(activity, alias)
if (result) {
freeCallIfSaved()
resolve()
} else {
freeCallIfSaved()
reject("removeAll: failure")
}
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun removeItem(call: PluginCall) {
saveCall(call)
with (call) {
try {
assertReceived(Param.ALIAS, Param.IS_PARANOIA, Param.FILE_KEY)

Storage(context, alias, isParanoia).removeString(key, {
logDebug("delete: success")
freeCallIfSaved()
resolve()
}, {
logDebug("delete: failure")
freeCallIfSaved()
reject(it.toString())
})
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun destroy(call: PluginCall) {
saveCall(call)
with (call) {
try {
val result = Storage.destroy(activity)
if (result) {
freeCallIfSaved()
resolve()
} else {
freeCallIfSaved()
reject("destroy: failure")
}
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun setupParanoiaPassword(call: PluginCall) {
saveCall(call)
with (call) {
try {
assertReceived(Param.ALIAS, Param.IS_PARANOIA)

Storage(context, alias, isParanoia).setupParanoiaPassword({
logDebug("paranoia setup: success")
freeCallIfSaved()
resolve()
}, {
logDebug("paranoia setup: failure")
freeCallIfSaved()
reject(it.toString())
})
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
}

@PluginMethod
fun setupRecoveryPassword(call: PluginCall) {
saveCall(call)
with (call) {
try {
assertReceived(Param.ALIAS, Param.IS_PARANOIA, Param.FILE_KEY, Param.VALUE)

Storage(context, alias, isParanoia).writeRecoverableString(key, value, {
logDebug("written recoverable: success")
freeCallIfSaved()
resolveWithData("recoveryKey" to it)
}, {
logDebug("written recoverable: failure")
freeCallIfSaved()
reject(it.toString())
}, { showAuthenticationScreen(call, it) })
}, createAuthenticationRequestedHandler(call))
} catch (e: Exception) {
freeCallIfSaved()
reject(e.toString())
}
}
Expand All @@ -231,13 +268,7 @@ class SecurityUtils : Plugin() {

@PluginMethod
fun authenticate(call: PluginCall) {
authenticate(call) {
if (it) {
call.resolve()
} else {
call.reject("Authentication failed")
}
}
authenticateOrContinue(AuthOrigin.VAULT, call, { call.resolve() }, { call.reject("Authentication failed") })
}

@PluginMethod
Expand Down Expand Up @@ -307,7 +338,7 @@ class SecurityUtils : Plugin() {
override fun handleOnResume() {
super.handleOnResume()
if (automaticLocalAuthentication) {
authenticate()
authenticateOrContinue(AuthOrigin.VAULT, savedCall, { freeCallIfSaved() })
}
}

Expand Down Expand Up @@ -339,22 +370,55 @@ class SecurityUtils : Plugin() {
}
}

private fun authenticate(call: PluginCall? = null, onResult: ((Boolean) -> Unit)? = null) {
private fun createAuthenticationRequestedHandler(call: PluginCall): (Int, () -> Unit) -> Unit =
{ attemptNo, onAuthenticated ->
authenticate(AuthOrigin.SYSTEM, attemptNo, call, onAuthenticated)
}

private fun authenticateOrContinue(origin: AuthOrigin, call: PluginCall? = null, onAuthenticated: (() -> Unit)? = null, onFailure: (() -> Unit)? = null) {
if (!needsAuthentication) {
onResult?.invoke(true)
onAuthenticated?.invoke()
} else {
showAuthenticationScreen(call, onAuthenticated = {
isAuthenticated = true
lastBackgroundDate = null
onResult?.invoke(true)
}, onFailure = {
isAuthenticated = false
lastBackgroundDate = null
onResult?.invoke(false)
})
authenticate(
origin,
++authTries,
call,
{
onAuthenticated?.invoke()
authTries = 0
},
onFailure
)
}
}

private fun authenticate(
origin: AuthOrigin,
attemptNo: Int,
call: PluginCall? = null,
onAuthenticated: (() -> Unit)? = null,
onFailure: (() -> Unit)? = null
) {
if (call != null && attemptNo > MAX_AUTH_TRIES) {
val error = when (origin) {
AuthOrigin.VAULT -> PluginError.maxAuthenticationRetriesVault
AuthOrigin.SYSTEM -> PluginError.maxAuthenticationRetriesSystem
}
call.reject(error.toString())
return
}

showAuthenticationScreen(call, onAuthenticated = {
isAuthenticated = true
lastBackgroundDate = null
onAuthenticated?.invoke()
}, onFailure = {
isAuthenticated = false
lastBackgroundDate = null
onFailure?.invoke()
})
}

private val PluginCall.alias: String
get() = getString(Param.ALIAS)

Expand Down Expand Up @@ -400,5 +464,6 @@ class SecurityUtils : Plugin() {
const val REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 1

private const val PREFERENCES_KEY_AUTOMATIC_AUTHENTICATION = "autoauth"
private const val MAX_AUTH_TRIES = 3
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package it.airgap.vault.plugin.securityutils.storage

import android.os.Build
import android.security.keystore.UserNotAuthenticatedException
import java.io.*
import java.io.File
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.charset.*
import java.nio.charset.CharacterCodingException
import java.nio.charset.Charset
import java.nio.charset.CharsetDecoder
import java.nio.charset.CodingErrorAction
import java.security.Key
import java.security.MessageDigest
import javax.crypto.Cipher
Expand All @@ -18,8 +22,9 @@ import kotlin.concurrent.thread
* Created by Dominik on 19.01.2018.
*/
class SecureFileStorage(private val masterSecret: Key?, private val salt: ByteArray, private val baseDir: File) {
private var authAttemptNo: Int = 0

fun read(fileKey: String, secret: ByteArray = "".toByteArray(), success: (String) -> Unit, error: (Exception) -> Unit, requestAuthentication: (() -> Unit) -> Unit) {
fun read(fileKey: String, secret: ByteArray = "".toByteArray(), success: (String) -> Unit, error: (Exception) -> Unit, requestAuthentication: (Int, () -> Unit) -> Unit) {
thread {
try {
SecureFile(baseDir, hashForKey(fileKey)).input { fileInputStream ->
Expand Down Expand Up @@ -47,14 +52,15 @@ class SecureFileStorage(private val masterSecret: Key?, private val salt: ByteAr
specificSecretCipher.init(Cipher.DECRYPT_MODE, specificSecretKey, IvParameterSpec(ivForKey(fileKey)))

val secretCipherInputStream = CipherInputStream(fsCipherInputStream, specificSecretCipher)

val fileValue = secretCipherInputStream.readTextAndClose()

authAttemptNo = 0
success(fileValue)
}
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e is UserNotAuthenticatedException) {
requestAuthentication { read(fileKey, secret, success, error, requestAuthentication) }
requestAuthentication(++authAttemptNo) { read(fileKey, secret, success, error, requestAuthentication) }
} else {
error(e)
}
Expand All @@ -65,7 +71,7 @@ class SecureFileStorage(private val masterSecret: Key?, private val salt: ByteAr
}
}

fun write(fileKey: String, fileData: String, secret: ByteArray = "".toByteArray(), success: () -> Unit, error: (Exception) -> Unit, requestAuthentication: (() -> Unit) -> Unit) {
fun write(fileKey: String, fileData: String, secret: ByteArray = "".toByteArray(), success: () -> Unit, error: (Exception) -> Unit, requestAuthentication: (Int, () -> Unit) -> Unit) {
thread {
try {
SecureFile(baseDir, hashForKey(fileKey)).output { fileOutputStream ->
Expand Down Expand Up @@ -93,12 +99,13 @@ class SecureFileStorage(private val masterSecret: Key?, private val salt: ByteAr
it.flush()
}

authAttemptNo = 0
success()
}
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e is UserNotAuthenticatedException) {
requestAuthentication { write(fileKey, fileData, secret, success, error, requestAuthentication) }
requestAuthentication(++authAttemptNo) { write(fileKey, fileData, secret, success, error, requestAuthentication) }
} else {
error(e)
}
Expand Down
Loading

0 comments on commit d6a4c99

Please sign in to comment.