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

View File

@ -22,6 +22,54 @@ import java.util.HashMap;
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
public class Pigeon { 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> { public interface Result<T> {
void success(T result); void success(T result);
void error(Throwable error); void error(Throwable error);
@ -29,12 +77,33 @@ public class Pigeon {
private static class OathApiCodec extends StandardMessageCodec { private static class OathApiCodec extends StandardMessageCodec {
public static final OathApiCodec INSTANCE = new OathApiCodec(); public static final OathApiCodec INSTANCE = new OathApiCodec();
private 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.*/ /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
public interface OathApi { public interface OathApi {
void reset(Result<Void> result); 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 setPassword(@Nullable String currentPassword, @NonNull String newPassword, Result<Void> result);
void unsetPassword(@NonNull String currentPassword, Result<Void> result); void unsetPassword(@NonNull String currentPassword, Result<Void> result);
void forgetPassword(Result<Void> result); void forgetPassword(Result<Void> result);
@ -96,8 +165,8 @@ public class Pigeon {
if (rememberArg == null) { if (rememberArg == null) {
throw new NullPointerException("rememberArg unexpectedly null."); throw new NullPointerException("rememberArg unexpectedly null.");
} }
Result<Boolean> resultCallback = new Result<Boolean>() { Result<UnlockResponse> resultCallback = new Result<UnlockResponse>() {
public void success(Boolean result) { public void success(UnlockResponse result) {
wrapped.put("result", result); wrapped.put("result", result);
reply.reply(wrapped); 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 package com.yubico.authenticator
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.viewModels import androidx.activity.viewModels

View File

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

View File

@ -54,4 +54,4 @@ fun Pair<Credential, Code?>.toJson(deviceId: String) = JsonObject(
"credential" to first.toJson(deviceId), "credential" to first.toJson(deviceId),
"code" to (second?.toJson() ?: JsonNull) "code" to (second?.toJson() ?: JsonNull)
) )
) )

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/foundation.dart' show WriteBuffer, ReadBuffer;
import 'package:flutter/services.dart'; 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 { class _OathApiCodec extends StandardMessageCodec {
const _OathApiCodec(); 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 { 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?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.OathApi.unlock', codec, binaryMessenger: _binaryMessenger); 'dev.flutter.pigeon.OathApi.unlock', codec, binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap = final Map<Object?, Object?>? replyMap =
@ -67,7 +113,7 @@ class OathApi {
message: 'Host platform returned null value for non-null return value.', message: 'Host platform returned null value for non-null return value.',
); );
} else { } 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, Future<Pair<bool, bool>> unlock(String password,
{bool remember = false}) async { {bool remember = false}) async {
try { 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'); _log.config('applet unlocked');
setData(state.value!.copyWith(locked: false)); setData(state.value!.copyWith(locked: false));
} }
return Pair(unlockSuccess, false); // TODO: provide correct second param return Pair(unlocked, remembered);
} on PlatformException catch (e) { } on PlatformException catch (e) {
_log.config('Calling unlock failed with exception: $e'); _log.config('Calling unlock failed with exception: $e');
return Pair(false, false); return Pair(false, false);

View File

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