Skip to content

Commit

Permalink
Split authenticationValidityDurationSeconds between android and iOS a…
Browse files Browse the repository at this point in the history
  • Loading branch information
astorblacker committed Mar 1, 2023
1 parent f514667 commit 9ae47c0
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import mu.KotlinLogging
import java.io.File
import java.io.IOException
import javax.crypto.Cipher
import kotlin.time.Duration

private val logger = KotlinLogging.logger {}

data class InitOptions(
val authenticationValidityDurationSeconds: Int = -1,
val androidAuthenticationValidityDuration: Duration? = null,
val authenticationRequired: Boolean = true,
val androidBiometricOnly: Boolean = true
)
Expand Down Expand Up @@ -44,20 +45,22 @@ class BiometricStorageFile(
setIsStrongBoxBacked(useStrongBox)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (options.authenticationValidityDurationSeconds == -1) {
if (options.androidAuthenticationValidityDuration == null) {
setUserAuthenticationParameters(
0,
KeyProperties.AUTH_BIOMETRIC_STRONG
)
} else {
setUserAuthenticationParameters(
options.authenticationValidityDurationSeconds,
options.androidAuthenticationValidityDuration.inWholeSeconds.toInt(),
KeyProperties.AUTH_DEVICE_CREDENTIAL or KeyProperties.AUTH_BIOMETRIC_STRONG
)
}
} else {
@Suppress("DEPRECATION")
setUserAuthenticationValidityDurationSeconds(options.authenticationValidityDurationSeconds)
setUserAuthenticationValidityDurationSeconds(
options.androidAuthenticationValidityDuration?.inWholeSeconds?.toInt() ?: -1
)
}
}

Expand All @@ -74,8 +77,8 @@ class BiometricStorageFile(
}

private fun validateOptions() {
if (options.authenticationValidityDurationSeconds == -1 && !options.androidBiometricOnly) {
throw IllegalArgumentException("when authenticationValidityDurationSeconds is -1, androidBiometricOnly must be true")
if (options.androidAuthenticationValidityDuration == null && !options.androidBiometricOnly) {
throw IllegalArgumentException("when androidAuthenticationValidityDuration is null, androidBiometricOnly must be true")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import java.io.StringWriter
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.crypto.Cipher
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

private val logger = KotlinLogging.logger {}

Expand Down Expand Up @@ -176,7 +178,7 @@ class BiometricStoragePlugin : FlutterPlugin, ActivityAware, MethodCallHandler {
CipherMode.Decrypt -> cipherForDecrypt()
}

val cipher = if (options.authenticationValidityDurationSeconds > -1) {
val cipher = if (options.androidAuthenticationValidityDuration != null) {
null
} else try {
cipherForMode()
Expand Down Expand Up @@ -222,7 +224,7 @@ class BiometricStoragePlugin : FlutterPlugin, ActivityAware, MethodCallHandler {

val options = call.argument<Map<String, Any>>("options")?.let { it ->
InitOptions(
authenticationValidityDurationSeconds = it["authenticationValidityDurationSeconds"] as Int,
androidAuthenticationValidityDuration = (it["androidAuthenticationValidityDurationSeconds"] as Int?)?.seconds,
authenticationRequired = it["authenticationRequired"] as Boolean,
androidBiometricOnly = it["androidBiometricOnly"] as Boolean,
)
Expand Down Expand Up @@ -398,9 +400,9 @@ class BiometricStoragePlugin : FlutterPlugin, ActivityAware, MethodCallHandler {
promptBuilder.setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_STRONG)
}

if (cipher == null || options.authenticationValidityDurationSeconds >= 0) {
// if authenticationValidityDurationSeconds is not -1 we can't use a CryptoObject
logger.debug { "Authenticating without cipher. ${options.authenticationValidityDurationSeconds}" }
if (cipher == null || options.androidAuthenticationValidityDuration != null) {
// if androidAuthenticationValidityDuration is not null we can't use a CryptoObject
logger.debug { "Authenticating without cipher. ${options.androidAuthenticationValidityDuration}" }
prompt.authenticate(promptBuilder.build())
} else {
prompt.authenticate(promptBuilder.build(), BiometricPrompt.CryptoObject(cipher))
Expand Down
45 changes: 42 additions & 3 deletions lib/src/biometric_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,46 @@ class AuthException implements Exception {

class StorageFileInitOptions {
StorageFileInitOptions({
Duration? androidAuthenticationValidityDuration,
Duration? iosTouchIDAuthenticationAllowableReuseDuration,
this.iosTouchIDAuthenticationForceReuseContextDuration,
this.authenticationValidityDurationSeconds = -1,
this.authenticationRequired = true,
this.androidBiometricOnly = true,
});
}) : androidAuthenticationValidityDuration =
androidAuthenticationValidityDuration ??
(authenticationValidityDurationSeconds <= 0
? null
: Duration(seconds: authenticationValidityDurationSeconds)),
iosTouchIDAuthenticationAllowableReuseDuration =
iosTouchIDAuthenticationAllowableReuseDuration ??
(authenticationValidityDurationSeconds <= 0
? null
: Duration(seconds: authenticationValidityDurationSeconds));

@Deprecated('use androidAuthenticationValidityDuration instead')

final int authenticationValidityDurationSeconds;

/// see https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationParameters(int,%20int)
final Duration? androidAuthenticationValidityDuration;

/// see https://developer.apple.com/documentation/localauthentication/lacontext/1622329-touchidauthenticationallowablere
/// > If the user unlocks the device using Touch ID within the specified time interval, then authentication for the receiver succeeds automatically, without prompting the user for Touch ID. This bypasses a scenario where the user unlocks the device and then is almost immediately prompted for another fingerprint.
/// and https://developer.apple.com/documentation/localauthentication/accessing_keychain_items_with_face_id_or_touch_id
/// > Note that this grace period applies specifically to device unlock with Touch ID, not keychain retrieval authentications
///
/// If you want to avoid requiring authentication after a successful
/// keychain retrieval see [iosTouchIDAuthenticationForceReuseContextDuration]
final Duration? iosTouchIDAuthenticationAllowableReuseDuration;

/// To prevent forcing the user to authenticate again after unlocking once
/// we can reuse the `LAContext` object for the given amount of time.
/// see https://github.com/authpass/biometric_storage/pull/73
/// This is pretty much undocumented behavior, but works similar to
/// `androidAuthenticationValidityDuration`.
final Duration? iosTouchIDAuthenticationForceReuseContextDuration;

/// Whether an authentication is required. if this is
/// false NO BIOMETRIC CHECK WILL BE PERFORMED! and the value
/// will simply be save encrypted. (default: true)
Expand All @@ -97,14 +130,20 @@ class StorageFileInitOptions {
/// On Android < 30 this will always be ignored. (always `true`)
/// https://github.com/authpass/biometric_storage/issues/12#issuecomment-900358154
///
/// Also: this **must** be `true` if [authenticationValidityDurationSeconds]
/// is `-1`.
/// Also: this **must** be `true` if [androidAuthenticationValidityDuration]
/// is null.
/// https://github.com/authpass/biometric_storage/issues/12#issuecomment-902508609
final bool androidBiometricOnly;

Map<String, dynamic> toJson() => <String, dynamic>{
'authenticationValidityDurationSeconds':
authenticationValidityDurationSeconds,
'androidAuthenticationValidityDurationSeconds':
androidAuthenticationValidityDuration?.inSeconds,
'iosTouchIDAuthenticationAllowableReuseDurationSeconds':
iosTouchIDAuthenticationAllowableReuseDuration?.inSeconds,
'iosTouchIDAuthenticationForceReuseContextDurationSeconds':
iosTouchIDAuthenticationForceReuseContextDuration?.inSeconds,
'authenticationRequired': authenticationRequired,
'androidBiometricOnly': androidBiometricOnly,
};
Expand Down
42 changes: 31 additions & 11 deletions macos/Classes/BiometricStorageImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ struct StorageMethodCall {

class InitOptions {
init(params: [String: Any]) {
authenticationValidityDurationSeconds = params["authenticationValidityDurationSeconds"] as? Int
iosTouchIDAuthenticationAllowableReuseDuration = params["iosTouchIDAuthenticationAllowableReuseDurationSeconds"] as? Int
iosTouchIDAuthenticationForceReuseContextDuration = params["iosTouchIDAuthenticationForceReuseContextDurationSeconds"] as? Int
authenticationRequired = params["authenticationRequired"] as? Bool
}
let authenticationValidityDurationSeconds: Int!
let iosTouchIDAuthenticationAllowableReuseDuration: Int?
let iosTouchIDAuthenticationForceReuseContextDuration: Int?
let authenticationRequired: Bool!
}

Expand Down Expand Up @@ -148,23 +150,41 @@ class BiometricStorageImpl {
}
}

typealias StoredContext = (context: LAContext, expireAt: Date)

class BiometricStorageFile {
private let name: String
private let initOptions: InitOptions
private var context: LAContext { get {
let context = LAContext()
if (initOptions.authenticationRequired) {
if initOptions.authenticationValidityDurationSeconds > 0 {
if #available(OSX 10.12, *) {
context.touchIDAuthenticationAllowableReuseDuration = Double(initOptions.authenticationValidityDurationSeconds)
private var _context: StoredContext?
private var context: LAContext {
get {
if let context = _context {
if context.expireAt.timeIntervalSinceNow < 0 {
// already expired.
_context = nil
} else {
// Fallback on earlier versions
hpdebug("Pre OSX 10.12 no touchIDAuthenticationAllowableReuseDuration available. ignoring.")
return context.context
}
}
let context = LAContext()
if (initOptions.authenticationRequired) {
if let duration = initOptions.iosTouchIDAuthenticationAllowableReuseDuration {
if #available(OSX 10.12, *) {
context.touchIDAuthenticationAllowableReuseDuration = Double(duration)
} else {
// Fallback on earlier versions
hpdebug("Pre OSX 10.12 no touchIDAuthenticationAllowableReuseDuration available. ignoring.")
}
}

if let duration = initOptions.iosTouchIDAuthenticationForceReuseContextDuration {
_context = (context: context, expireAt: Date(timeIntervalSinceNow: Double(duration)))
}
}
return context
}
return context
} }
}
private let storageError: StorageError

init(name: String, initOptions: InitOptions, storageError: @escaping StorageError) {
Expand Down

0 comments on commit 9ae47c0

Please sign in to comment.