This commit is contained in:
Adam Velebil 2022-03-25 09:15:56 +01:00
commit e11ca5b531
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
13 changed files with 514 additions and 55 deletions

View File

@ -3,6 +3,7 @@ package com.yubico.authenticator.api
import com.yubico.authenticator.MainViewModel
import com.yubico.authenticator.api.Pigeon.OathApi
import com.yubico.authenticator.api.Pigeon.Result
import com.yubico.authenticator.api.Pigeon.UnlockResponse
class OathApiImpl(private val viewModel: MainViewModel) : OathApi {
@ -13,7 +14,7 @@ class OathApiImpl(private val viewModel: MainViewModel) : OathApi {
override fun unlock(
password: String,
remember: Boolean,
result: Result<Boolean>
result: Result<UnlockResponse>
) {
viewModel.unlockOathSession(password, remember, result)
}

View File

@ -22,6 +22,54 @@ import java.util.HashMap;
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
public class Pigeon {
/** Generated class from Pigeon that represents data sent in messages. */
public static class UnlockResponse {
private @Nullable Boolean isUnlocked;
public @Nullable Boolean getIsUnlocked() { return isUnlocked; }
public void setIsUnlocked(@Nullable Boolean setterArg) {
this.isUnlocked = setterArg;
}
private @Nullable Boolean isRemembered;
public @Nullable Boolean getIsRemembered() { return isRemembered; }
public void setIsRemembered(@Nullable Boolean setterArg) {
this.isRemembered = setterArg;
}
public static class Builder {
private @Nullable Boolean isUnlocked;
public @NonNull Builder setIsUnlocked(@Nullable Boolean setterArg) {
this.isUnlocked = setterArg;
return this;
}
private @Nullable Boolean isRemembered;
public @NonNull Builder setIsRemembered(@Nullable Boolean setterArg) {
this.isRemembered = setterArg;
return this;
}
public @NonNull UnlockResponse build() {
UnlockResponse pigeonReturn = new UnlockResponse();
pigeonReturn.setIsUnlocked(isUnlocked);
pigeonReturn.setIsRemembered(isRemembered);
return pigeonReturn;
}
}
@NonNull Map<String, Object> toMap() {
Map<String, Object> toMapResult = new HashMap<>();
toMapResult.put("isUnlocked", isUnlocked);
toMapResult.put("isRemembered", isRemembered);
return toMapResult;
}
static @NonNull UnlockResponse fromMap(@NonNull Map<String, Object> map) {
UnlockResponse pigeonResult = new UnlockResponse();
Object isUnlocked = map.get("isUnlocked");
pigeonResult.setIsUnlocked((Boolean)isUnlocked);
Object isRemembered = map.get("isRemembered");
pigeonResult.setIsRemembered((Boolean)isRemembered);
return pigeonResult;
}
}
public interface Result<T> {
void success(T result);
void error(Throwable error);
@ -29,12 +77,33 @@ public class Pigeon {
private static class OathApiCodec extends StandardMessageCodec {
public static final OathApiCodec INSTANCE = new OathApiCodec();
private OathApiCodec() {}
@Override
protected Object readValueOfType(byte type, ByteBuffer buffer) {
switch (type) {
case (byte)128:
return UnlockResponse.fromMap((Map<String, Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
}
}
@Override
protected void writeValue(ByteArrayOutputStream stream, Object value) {
if (value instanceof UnlockResponse) {
stream.write(128);
writeValue(stream, ((UnlockResponse) value).toMap());
} else
{
super.writeValue(stream, value);
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
public interface OathApi {
void reset(Result<Void> result);
void unlock(@NonNull String password, @NonNull Boolean remember, Result<Boolean> result);
void unlock(@NonNull String password, @NonNull Boolean remember, Result<UnlockResponse> result);
void setPassword(@Nullable String currentPassword, @NonNull String newPassword, Result<Void> result);
void unsetPassword(@NonNull String currentPassword, Result<Void> result);
void forgetPassword(Result<Void> result);
@ -96,8 +165,8 @@ public class Pigeon {
if (rememberArg == null) {
throw new NullPointerException("rememberArg unexpectedly null.");
}
Result<Boolean> resultCallback = new Result<Boolean>() {
public void success(Boolean result) {
Result<UnlockResponse> resultCallback = new Result<UnlockResponse>() {
public void success(UnlockResponse result) {
wrapped.put("result", result);
reply.reply(wrapped);
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2022, Yubico AB. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package com.yubico.authenticator
import com.yubico.authenticator.keystore.KeyProvider
import com.yubico.yubikit.oath.AccessKey
class KeyManager(private val permStore: KeyProvider, private val memStore: KeyProvider) {
/**
* @return true if this deviceId is stored in permanent KeyStore
*/
fun isRemembered(deviceId: String) = permStore.hasKey(deviceId)
fun getKey(deviceId: String): AccessKey? {
return if (permStore.hasKey(deviceId)) {
permStore.getKey(deviceId)
} else {
memStore.getKey(deviceId)
}
}
fun addKey(deviceId: String, secret: ByteArray, remember: Boolean) {
if (remember) {
memStore.removeKey(deviceId)
permStore.putKey(deviceId, secret)
} else {
permStore.removeKey(deviceId)
memStore.putKey(deviceId, secret)
}
}
fun removeKey(deviceId: String) {
memStore.removeKey(deviceId)
permStore.removeKey(deviceId)
}
fun clearAll() {
memStore.clearAll()
permStore.clearAll()
}
}

View File

@ -1,6 +1,5 @@
package com.yubico.authenticator
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels

View File

@ -6,10 +6,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yubico.authenticator.api.Pigeon
import com.yubico.authenticator.data.device.toJson
import com.yubico.authenticator.data.oath.calculateSteamCode
import com.yubico.authenticator.data.oath.idAsString
import com.yubico.authenticator.data.oath.isSteamCredential
import com.yubico.authenticator.data.oath.toJson
import com.yubico.authenticator.data.oath.*
import com.yubico.authenticator.keystore.ClearingMemProvider
import com.yubico.authenticator.keystore.KeyStoreProvider
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
import com.yubico.yubikit.core.Logger
@ -45,9 +44,11 @@ class MainViewModel : ViewModel() {
val handleYubiKey: LiveData<Boolean> = _handleYubiKey
val yubiKeyDevice = MutableLiveData<YubiKeyDevice?>()
private var isUsbKeyConnected: Boolean = false
private var _isUsbKey: Boolean = false
private var _previousNfcDeviceId = ""
private var _oathSessionPassword: CharArray? = null
private val _memoryKeyProvider = ClearingMemProvider()
private val _keyManager = KeyManager(KeyStoreProvider(), _memoryKeyProvider)
private var _operationContext = OperationContext.Oath
@ -100,7 +101,23 @@ class MainViewModel : ViewModel() {
val oathSessionData = suspendCoroutine<String> {
device.requestConnection(SmartCardConnection::class.java) { result ->
val oathSession = OathSession(result.value)
val isRemembered = false
val deviceId = oathSession.deviceId
_previousNfcDeviceId = if (device is NfcYubiKeyDevice) {
if (deviceId != _previousNfcDeviceId) {
// devices are different, clear access key for previous device
_memoryKeyProvider.removeKey(_previousNfcDeviceId)
}
deviceId
} else {
""
}
// calling unlock session will remove invalid access keys
tryToUnlockOathSession(oathSession)
val isRemembered = _keyManager.isRemembered(oathSession.deviceId)
val oathSessionData = oathSession
.toJson(isRemembered)
.toString()
@ -115,9 +132,7 @@ class MainViewModel : ViewModel() {
val sendOathCodes = suspendCoroutine<String> {
device.requestConnection(SmartCardConnection::class.java) { result ->
val session = OathSession(result.value)
val isLocked = isOathSessionLocked(session)
if (!isLocked) {
if (tryToUnlockOathSession(session)) {
val resultJson = calculateOathCodes(session)
.toJson(session.deviceId)
.toString()
@ -136,7 +151,7 @@ class MainViewModel : ViewModel() {
withContext(Dispatchers.IO) {
pendingYubiKeyAction.value?.let {
_pendingYubiKeyAction.postValue(null)
if (!isUsbKeyConnected) {
if (!_isUsbKey) {
withContext(Dispatchers.Main) {
requestHideDialog()
}
@ -147,7 +162,7 @@ class MainViewModel : ViewModel() {
suspend fun yubikeyAttached(device: YubiKeyDevice) {
isUsbKeyConnected = device is UsbYubiKeyDevice
_isUsbKey = device is UsbYubiKeyDevice
withContext(Dispatchers.IO) {
if (pendingYubiKeyAction.value != null) {
@ -176,13 +191,11 @@ class MainViewModel : ViewModel() {
}
fun yubikeyDetached() {
if (isUsbKeyConnected) {
// forget the current password only for usb keys
_oathSessionPassword = null
if (_isUsbKey) {
// clear keys from memory
_memoryKeyProvider.clearAll()
_fManagementApi.updateDeviceInfo("") {}
}
}
fun onDialogClosed(result: Pigeon.Result<Void>) {
@ -206,8 +219,7 @@ class MainViewModel : ViewModel() {
}
private fun <T> withUnlockedSession(session: OathSession, block: (OathSession) -> T): T {
val isLocked = isOathSessionLocked(session)
if (isLocked) {
if (!tryToUnlockOathSession(session)) {
throw Exception("Session is locked")
}
return block(session)
@ -320,9 +332,9 @@ class MainViewModel : ViewModel() {
throw Exception("Provided current password is invalid")
}
}
val newPass = password.toCharArray()
session.setPassword(newPass)
_oathSessionPassword = newPass
val accessKey = session.deriveAccessKey(password.toCharArray())
session.setAccessKey(accessKey)
_keyManager.addKey(session.deviceId, accessKey, false)
Logger.d("Successfully set password")
}
} catch (cause: Throwable) {
@ -341,7 +353,7 @@ class MainViewModel : ViewModel() {
// test current password sent by the user
if (session.unlock(currentPassword.toCharArray())) {
session.deleteAccessKey()
_oathSessionPassword = null
_keyManager.removeKey(session.deviceId)
Logger.d("Successfully unset password")
result.success(null)
return@useOathSession
@ -369,7 +381,7 @@ class MainViewModel : ViewModel() {
fun refreshOathCodes(result: Pigeon.Result<String>) {
viewModelScope.launch(Dispatchers.IO) {
try {
if (!isUsbKeyConnected) {
if (!_isUsbKey) {
throw Exception("Cannot refresh for nfc key")
}
@ -411,28 +423,33 @@ class MainViewModel : ViewModel() {
fun unlockOathSession(
password: String,
remember: Boolean,
result: Pigeon.Result<Boolean>
result: Pigeon.Result<Pigeon.UnlockResponse>
) {
viewModelScope.launch(Dispatchers.IO) {
try {
var codes: String? = null
useOathSession("Unlocking", true) {
_oathSessionPassword = password.toCharArray()
val isLocked = isOathSessionLocked(it)
val accessKey = it.deriveAccessKey(password.toCharArray())
_keyManager.addKey(it.deviceId, accessKey, remember)
if (!isLocked) {
val response = Pigeon.UnlockResponse().apply {
isUnlocked = tryToUnlockOathSession(it)
isRemembered = _keyManager.isRemembered(it.deviceId)
}
if (response.isUnlocked == true) {
codes = calculateOathCodes(it)
.toJson(it.deviceId)
.toString()
}
result.success(!isLocked)
result.success(response)
}
codes?.let {
viewModelScope.launch(Dispatchers.Main) {
_fOathApi.updateOathCredentials(it) {}
}
}
} catch (cause: Throwable) {
result.error(cause)
@ -459,7 +476,7 @@ class MainViewModel : ViewModel() {
queryUserToTap: Boolean,
action: (OathSession) -> T
) = suspendCoroutine<T> { outer ->
if (queryUserToTap && !isUsbKeyConnected) {
if (queryUserToTap && !_isUsbKey) {
viewModelScope.launch(Dispatchers.Main) {
requestShowDialog(title)
}
@ -484,30 +501,33 @@ class MainViewModel : ViewModel() {
}
/**
* returns true if the session cannot be unlocked (either we don't have a password, or the password is incorrect
* Tries to unlocks [OathSession] with [AccessKey] stored in [KeyManager]. On failure clears
* relevant access keys from [KeyManager]
*
* returns false if we can unlock the session
* @return true if we the session is not locked or it was successfully unlocked, false otherwise
*/
private fun isOathSessionLocked(session: OathSession): Boolean {
private fun tryToUnlockOathSession(session: OathSession): Boolean {
if (!session.isLocked) {
return false
return true
}
if (_oathSessionPassword == null) {
return true // we have no password to unlock
}
val deviceId = session.deviceId
val accessKey = _keyManager.getKey(deviceId)
?: return false // we have no access key to unlock the session
val unlockSucceed = session.unlock(_oathSessionPassword!!)
val unlockSucceed = session.unlock(accessKey)
if (unlockSucceed) {
return false // we have everything to unlock the session
return true
}
_oathSessionPassword = null // reset the password as well as it did not work
return true // the unlock did not work, session is locked
_keyManager.removeKey(deviceId) // remove invalid access keys from [KeyManager]
return false // the unlock did not work, session is locked
}
fun forgetPassword(result: Pigeon.Result<Void>) {
_keyManager.clearAll()
Logger.d("Cleared all keys.")
result.success(null)
}

View File

@ -0,0 +1,53 @@
package com.yubico.authenticator.keystore
import android.security.keystore.KeyProperties
import com.yubico.yubikit.oath.AccessKey
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.schedule
class ClearingMemProvider : KeyProvider {
private var current: Pair<String, Mac>? = null
private var clearAllTask: TimerTask? = null
override fun hasKey(deviceId: String): Boolean = current?.first == deviceId
override fun getKey(deviceId: String): AccessKey? {
clearAllTask?.cancel()
clearAllTask = Timer("clear-memory-keys", false)
.schedule(5 * 60_000) {
clearAll()
}
current?.let {
if (it.first == deviceId) {
return MemStoredSigner(it.second)
}
}
return null
}
override fun putKey(deviceId: String, secret: ByteArray) {
current = Pair(deviceId,
Mac.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA1)
.apply {
init(SecretKeySpec(secret, algorithm))
}
)
}
override fun removeKey(deviceId: String) {
current = null
}
override fun clearAll() {
current = null
}
private inner class MemStoredSigner(val mac: Mac) : AccessKey {
override fun calculateResponse(challenge: ByteArray?): ByteArray? = mac.doFinal(challenge)
}
}

View File

@ -0,0 +1,11 @@
package com.yubico.authenticator.keystore
import com.yubico.yubikit.oath.AccessKey
interface KeyProvider {
fun hasKey(deviceId: String): Boolean
fun getKey(deviceId: String): AccessKey?
fun putKey(deviceId: String, secret: ByteArray)
fun removeKey(deviceId: String)
fun clearAll()
}

View File

@ -0,0 +1,53 @@
package com.yubico.authenticator.keystore
import android.security.keystore.KeyProperties
import android.security.keystore.KeyProtection
import com.yubico.yubikit.oath.AccessKey
import java.security.KeyStore
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class KeyStoreProvider : KeyProvider {
private val keystore = KeyStore.getInstance("AndroidKeyStore")
init {
keystore.load(null)
}
override fun hasKey(deviceId: String): Boolean = keystore.containsAlias(deviceId)
override fun getKey(deviceId: String): AccessKey? =
if (hasKey(deviceId)) {
KeyStoreStoredSigner(deviceId)
} else {
null
}
override fun putKey(deviceId: String, secret: ByteArray) {
keystore.setEntry(
deviceId,
KeyStore.SecretKeyEntry(
SecretKeySpec(secret, KeyProperties.KEY_ALGORITHM_HMAC_SHA1)
),
KeyProtection.Builder(KeyProperties.PURPOSE_SIGN).build()
)
}
override fun removeKey(deviceId: String) {
keystore.deleteEntry(deviceId)
}
override fun clearAll() {
keystore.aliases().asSequence().forEach { keystore.deleteEntry(it) }
}
private inner class KeyStoreStoredSigner(val deviceId: String) :
AccessKey {
val mac: Mac = Mac.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA1).apply {
init(keystore.getKey(deviceId, null))
}
override fun calculateResponse(challenge: ByteArray?): ByteArray? = mac.doFinal(challenge)
}
}

View File

@ -0,0 +1,129 @@
package com.yubico.authenticator
import com.yubico.authenticator.keystore.KeyProvider
import com.yubico.yubikit.oath.AccessKey
import org.junit.Assert
import org.junit.Before
import org.junit.Test
/**
* Implementation of [StoredSigner] which returns [secret] for any challenge.
*/
class MockStoredSigner(private val secret: ByteArray) : AccessKey {
override fun calculateResponse(challenge: ByteArray): ByteArray = secret
}
/**
* Implementation of [KeyProvider] backed by a [Map]
*/
class MockKeyProvider : KeyProvider {
private val map: MutableMap<String, AccessKey> = mutableMapOf()
override fun hasKey(deviceId: String): Boolean = map[deviceId] != null
override fun getKey(deviceId: String): AccessKey? = map[deviceId]
override fun putKey(deviceId: String, secret: ByteArray) {
map[deviceId] = MockStoredSigner(secret)
}
override fun removeKey(deviceId: String) {
map.remove(deviceId)
}
override fun clearAll() {
map.clear()
}
}
class KeyManagerTest {
private val device1Id = "d1"
private val device2Id = "d2"
private val secret1 = "secret".toByteArray()
private val secret2 = "secret2".toByteArray()
private lateinit var permKeyProvider: KeyProvider
private lateinit var memKeyProvider: KeyProvider
private lateinit var keyManager: KeyManager
@Before
fun setUp() {
permKeyProvider = MockKeyProvider()
memKeyProvider = MockKeyProvider()
keyManager = KeyManager(permKeyProvider, memKeyProvider)
}
@Test
fun `adds secret to memory key provider`() {
keyManager.addKey(device1Id, secret1, false)
Assert.assertFalse(keyManager.isRemembered(device1Id))
}
@Test
fun `adds secret to permanent key provider`() {
keyManager.addKey(device1Id, secret1, true)
Assert.assertTrue(keyManager.isRemembered(device1Id))
}
@Test
fun `returns added key`() {
keyManager.addKey(device1Id, secret1, true)
val key1 = keyManager.getKey(device1Id)
Assert.assertNotNull(key1)
Assert.assertEquals(secret1, key1!!.calculateResponse(byteArrayOf()))
keyManager.addKey(device2Id, secret2, false)
val key2 = keyManager.getKey(device2Id)
Assert.assertNotNull(key2)
Assert.assertEquals(secret2, key2!!.calculateResponse(byteArrayOf()))
}
@Test
fun `associates keys with correct devices`() {
keyManager.addKey(device1Id, secret1, true)
Assert.assertNotNull(keyManager.getKey(device1Id))
Assert.assertNull(keyManager.getKey(device2Id))
keyManager.addKey(device2Id, secret2, false)
Assert.assertNotNull(keyManager.getKey(device2Id))
Assert.assertNotNull(keyManager.getKey(device1Id))
}
@Test
fun `clears keys for device`() {
keyManager.addKey(device1Id, secret1, true)
keyManager.addKey(device2Id, secret2, true)
keyManager.removeKey(device1Id)
Assert.assertNotNull(keyManager.getKey(device2Id))
Assert.assertNull(keyManager.getKey(device1Id))
}
@Test
fun `clears all keys`() {
keyManager.addKey(device1Id, secret1, true)
keyManager.addKey(device2Id, secret2, false)
keyManager.clearAll()
Assert.assertNull(keyManager.getKey(device1Id))
Assert.assertNull(keyManager.getKey(device2Id))
}
@Test
fun `can overwrite stored key`() {
keyManager.addKey(device1Id, secret1, true)
keyManager.addKey(device1Id, secret2, true)
val key1 = keyManager.getKey(device1Id)
Assert.assertEquals(secret2, key1!!.calculateResponse(byteArrayOf()))
keyManager.addKey(device1Id, secret1, true)
val key2 = keyManager.getKey(device1Id)
Assert.assertEquals(secret1, key2!!.calculateResponse(byteArrayOf()))
}
}

View File

@ -8,8 +8,54 @@ import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
import 'package:flutter/services.dart';
class UnlockResponse {
UnlockResponse({
this.isUnlocked,
this.isRemembered,
});
bool? isUnlocked;
bool? isRemembered;
Object encode() {
final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
pigeonMap['isUnlocked'] = isUnlocked;
pigeonMap['isRemembered'] = isRemembered;
return pigeonMap;
}
static UnlockResponse decode(Object message) {
final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
return UnlockResponse(
isUnlocked: pigeonMap['isUnlocked'] as bool?,
isRemembered: pigeonMap['isRemembered'] as bool?,
);
}
}
class _OathApiCodec extends StandardMessageCodec {
const _OathApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is UnlockResponse) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else
{
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return UnlockResponse.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class OathApi {
@ -44,7 +90,7 @@ class OathApi {
}
}
Future<bool> unlock(String arg_password, bool arg_remember) async {
Future<UnlockResponse> unlock(String arg_password, bool arg_remember) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.OathApi.unlock', codec, binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap =
@ -67,7 +113,7 @@ class OathApi {
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyMap['result'] as bool?)!;
return (replyMap['result'] as UnlockResponse?)!;
}
}

View File

@ -48,13 +48,16 @@ class _AndroidOathStateNotifier extends OathStateNotifier {
Future<Pair<bool, bool>> unlock(String password,
{bool remember = false}) async {
try {
final unlockSuccess = await _api.unlock(password, remember);
final unlockResponse = await _api.unlock(password, remember);
if (unlockSuccess) {
final unlocked = unlockResponse.isUnlocked == true;
final remembered = unlockResponse.isRemembered == true;
if (unlocked) {
_log.config('applet unlocked');
setData(state.value!.copyWith(locked: false));
}
return Pair(unlockSuccess, false); // TODO: provide correct second param
return Pair(unlocked, remembered);
} on PlatformException catch (e) {
_log.config('Calling unlock failed with exception: $e');
return Pair(false, false);

View File

@ -1,12 +1,17 @@
import 'package:pigeon/pigeon.dart';
class UnlockResponse {
bool? isUnlocked;
bool? isRemembered;
}
@HostApi()
abstract class OathApi {
@async
void reset();
@async
bool unlock(String password, bool remember);
UnlockResponse unlock(String password, bool remember);
@async
void setPassword(String? currentPassword, String newPassword);