mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add USB U2F support
This commit is contained in:
parent
651d1ea9b1
commit
a040e902d1
32 changed files with 1512 additions and 128 deletions
|
@ -43,6 +43,7 @@
|
|||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.nfc" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
|
||||
<application
|
||||
android:banner="@drawable/banner"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
inline fun <T> Collection<T>.some(predicate: (T) -> Boolean): Boolean {
|
||||
for (element in this) if (predicate(element)) return true
|
||||
|
||||
return false
|
||||
}
|
|
@ -236,6 +236,7 @@ object PendingIntentIds {
|
|||
const val UPDATE_STATUS = 5
|
||||
const val OPEN_UPDATER = 6
|
||||
const val U2F_NFC_DISCOVERY = 7
|
||||
const val U2F_USB_RESPONSE = 8
|
||||
val DYNAMIC_NOTIFICATION_RANGE = 100..10000
|
||||
|
||||
val PENDING_INTENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import io.timelimit.android.u2f.nfc.NFCU2FManager
|
||||
import io.timelimit.android.u2f.protocol.U2FDevice
|
||||
import io.timelimit.android.u2f.usb.UsbU2FManager
|
||||
|
||||
class U2fManager (context: Context) {
|
||||
companion object {
|
||||
|
@ -41,6 +42,7 @@ class U2fManager (context: Context) {
|
|||
}
|
||||
|
||||
private val nfc = NFCU2FManager(this, context)
|
||||
private val usb = UsbU2FManager(this, context)
|
||||
|
||||
private val deviceFoundListeners = mutableListOf<DeviceFoundListener>()
|
||||
|
||||
|
|
|
@ -32,11 +32,11 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.integration.platform.android.PendingIntentIds
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.u2f.U2fManager
|
||||
import io.timelimit.android.u2f.util.U2FId
|
||||
|
||||
class NFCU2FManager (val parent: U2fManager, context: Context) {
|
||||
companion object {
|
||||
|
@ -48,17 +48,23 @@ class NFCU2FManager (val parent: U2fManager, context: Context) {
|
|||
|
||||
private val nfcReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(p0: Context?, intent: Intent?) {
|
||||
val tagFromIntent: Tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) ?: return
|
||||
else
|
||||
intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return
|
||||
try {
|
||||
val tagFromIntent: Tag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java) ?: return
|
||||
else
|
||||
intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return
|
||||
|
||||
val isoDep: IsoDep = IsoDep.get(tagFromIntent) ?: return
|
||||
val isoDep: IsoDep = IsoDep.get(tagFromIntent) ?: return
|
||||
|
||||
parent.dispatchDeviceFound(NfcU2FDevice(isoDep))
|
||||
parent.dispatchDeviceFound(NfcU2FDevice(isoDep))
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "could not handle nfc broadcast", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val nfcReceiverAction = (0..6).map { IdGenerator.generateId() }.joinToString()
|
||||
private val nfcReceiverAction = U2FId.generate()
|
||||
private val nfcReceiverIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
PendingIntentIds.U2F_NFC_DISCOVERY,
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.nfc.tech.IsoDep
|
|||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.u2f.*
|
||||
import io.timelimit.android.u2f.protocol.U2FDeviceSession
|
||||
import io.timelimit.android.u2f.protocol.U2FRequest
|
||||
import io.timelimit.android.u2f.protocol.U2fRawResponse
|
||||
|
@ -35,7 +34,7 @@ class NfcU2FDeviceSession(private val tag: IsoDep): U2FDeviceSession {
|
|||
|
||||
override suspend fun execute(request: U2FRequest): U2fRawResponse = U2FThread.nfc.executeAndWait {
|
||||
try {
|
||||
var response = U2fRawResponse.decode(tag.transceive(request.encode()))
|
||||
var response = U2fRawResponse.decode(tag.transceive(request.encodeShort()))
|
||||
var fullPayload = response.payload
|
||||
|
||||
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-nfc-protocol-v1.2-ps-20170411.html
|
||||
|
@ -60,9 +59,7 @@ class NfcU2FDeviceSession(private val tag: IsoDep): U2FDeviceSession {
|
|||
|
||||
response = response.copy(payload = fullPayload)
|
||||
|
||||
if (response.status == 0x6A80.toUShort()) throw U2FException.BadKeyHandleException()
|
||||
if (response.status == 0x6985.toUShort()) throw U2FException.UserInteractionRequired()
|
||||
if (response.status != 0x9000.toUShort()) throw U2FException.DeviceException()
|
||||
response.throwIfNoSuccess()
|
||||
|
||||
response
|
||||
} catch (ex: TagLostException) {
|
||||
|
@ -75,4 +72,14 @@ class NfcU2FDeviceSession(private val tag: IsoDep): U2FDeviceSession {
|
|||
throw U2FException.CommunicationException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
U2FThread.nfc.submit {
|
||||
try {
|
||||
tag.close()
|
||||
} catch (ex: IOException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,9 @@
|
|||
*/
|
||||
package io.timelimit.android.u2f.protocol
|
||||
|
||||
interface U2FDeviceSession {
|
||||
import java.io.Closeable
|
||||
|
||||
interface U2FDeviceSession: Closeable {
|
||||
suspend fun execute(request: U2FRequest): U2fRawResponse
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ sealed class U2FRequest {
|
|||
abstract val p2: Byte
|
||||
abstract val payload: ByteArray
|
||||
|
||||
fun encode(): ByteArray {
|
||||
fun encodeShort(): ByteArray {
|
||||
val cla: Byte = 0
|
||||
|
||||
if (payload.size > 255) {
|
||||
|
@ -39,6 +39,23 @@ sealed class U2FRequest {
|
|||
) + payload + byteArrayOf(0)
|
||||
}
|
||||
|
||||
fun encodeExtended(): ByteArray {
|
||||
val cla: Byte = 0
|
||||
|
||||
if (payload.size > 65535) {
|
||||
throw U2FException.CommunicationException()
|
||||
}
|
||||
|
||||
return byteArrayOf(
|
||||
cla,
|
||||
ins,
|
||||
p1,
|
||||
p2,
|
||||
0,
|
||||
payload.size.ushr(8).toByte(),
|
||||
payload.size.toByte()
|
||||
) + payload + byteArrayOf(1, 0)
|
||||
}
|
||||
data class Register(
|
||||
val challenge: ByteArray,
|
||||
val applicationId: ByteArray
|
||||
|
|
|
@ -31,4 +31,11 @@ data class U2fRawResponse (
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun throwIfNoSuccess() {
|
||||
if (status == 0x6A80.toUShort()) throw U2FException.BadKeyHandleException()
|
||||
if (status == 0x6985.toUShort()) throw U2FException.UserInteractionRequired()
|
||||
if (status == 0x6700.toUShort()) throw U2FException.BadRequestLength()
|
||||
if (status != 0x9000.toUShort()) throw U2FException.DeviceException(status)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
class DisconnectReporter {
|
||||
private var didDisconnectInternal = false
|
||||
|
||||
val didDisconnect get() = didDisconnectInternal
|
||||
|
||||
fun reportDisconnect() { didDisconnectInternal = true }
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
sealed class UsbException(message: String): RuntimeException(message) {
|
||||
class InvalidDescriptorLengthException: UsbException("invalid descriptor length")
|
||||
class InvalidDescriptorTypeException: UsbException("invalid descriptor type")
|
||||
class WrongCounterException(type: String, expected: Int, found: Int): UsbException("expected $expected but found $found $type")
|
||||
class InvalidIndexException: UsbException("invalid index")
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
import android.content.Intent
|
||||
import android.hardware.usb.*
|
||||
import android.os.Build
|
||||
|
||||
val Intent.usbDevice
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
||||
this.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)!!
|
||||
else
|
||||
this.getParcelableExtra(UsbManager.EXTRA_DEVICE)!!
|
||||
|
||||
val UsbDevice.interfaces
|
||||
get() = (0 until this.interfaceCount).map { this.getInterface(it) }
|
||||
|
||||
val UsbInterface.endpoints
|
||||
get() = (0 until this.endpointCount).map { this.getEndpoint(it) }
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
import android.hardware.usb.UsbDevice
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class UsbPermissionRequestManager (
|
||||
private val sendRequest: (UsbDevice) -> Unit
|
||||
) {
|
||||
private val pendingRequests = mutableMapOf<String, MutableList<CancellableContinuation<Boolean>>>()
|
||||
|
||||
fun reportResult(device: UsbDevice, granted: Boolean) {
|
||||
synchronized(pendingRequests) { pendingRequests.remove(device.deviceName) }
|
||||
?.forEach { it.resume(granted) }
|
||||
}
|
||||
|
||||
suspend fun requestPermission(device: UsbDevice): Boolean {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation { removePendingRequest(device, continuation) }
|
||||
|
||||
addPendingRequest(device, continuation)
|
||||
|
||||
sendRequest(device)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPendingRequest(device: UsbDevice, listener: CancellableContinuation<Boolean>) = synchronized(pendingRequests) {
|
||||
pendingRequests.getOrPut(device.deviceName) { mutableListOf() }.add(listener)
|
||||
}
|
||||
|
||||
private fun removePendingRequest(device: UsbDevice, listener: CancellableContinuation<Boolean>) = synchronized(pendingRequests) {
|
||||
pendingRequests[device.deviceName]?.remove(listener)
|
||||
|
||||
if (pendingRequests[device.deviceName]?.isEmpty() == true) {
|
||||
pendingRequests.remove(device.deviceName)
|
||||
}
|
||||
}
|
||||
}
|
204
app/src/main/java/io/timelimit/android/u2f/usb/UsbU2FDevice.kt
Normal file
204
app/src/main/java/io/timelimit/android/u2f/usb/UsbU2FDevice.kt
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
import android.hardware.usb.UsbConstants
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.extensions.some
|
||||
import io.timelimit.android.u2f.protocol.U2FDevice
|
||||
import io.timelimit.android.u2f.protocol.U2FDeviceSession
|
||||
import io.timelimit.android.u2f.usb.descriptors.DeviceDescriptor
|
||||
import io.timelimit.android.u2f.usb.descriptors.ReportDescriptor
|
||||
import io.timelimit.android.u2f.util.U2FException
|
||||
import io.timelimit.android.u2f.util.U2FThread
|
||||
|
||||
data class UsbU2FDevice (
|
||||
val device: UsbDevice,
|
||||
val permissionRequestManager: UsbPermissionRequestManager,
|
||||
val usbManager: UsbManager,
|
||||
val disconnectReporter: DisconnectReporter
|
||||
): U2FDevice {
|
||||
companion object {
|
||||
private const val LOG_TAG = "UsbU2FDevice"
|
||||
private const val CLASS_HID = 3
|
||||
private const val ATTRIBUTES_INTERRUPT = 3
|
||||
private const val ATTRIBUTES_INTERRUPT_MASK = 3
|
||||
|
||||
// https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-hid-protocol-ps-20141009.html
|
||||
// http://esd.cs.ucr.edu/webres/usb11.pdf
|
||||
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||
fun from(
|
||||
device: UsbDevice,
|
||||
permissionRequestManager: UsbPermissionRequestManager,
|
||||
usbManager: UsbManager,
|
||||
disconnectReporter: DisconnectReporter
|
||||
): UsbU2FDevice? {
|
||||
for (usbInterface in device.interfaces) {
|
||||
// according to the U2F spec, there must be a input and output endpoint
|
||||
// according to the HID spec, the endpoints must be data interrupt endpoints
|
||||
// it looks like the U2F spec wants no subclass and protocol that is not zero
|
||||
// it looks like the HID spec does not allow more than one endpoint per type
|
||||
if (
|
||||
usbInterface.interfaceClass != CLASS_HID ||
|
||||
usbInterface.interfaceSubclass != 0 ||
|
||||
usbInterface.interfaceProtocol != 0 ||
|
||||
usbInterface.endpointCount != 2
|
||||
) continue
|
||||
|
||||
val hasInputEndpoint = usbInterface.endpoints.some {
|
||||
it.attributes and ATTRIBUTES_INTERRUPT_MASK == ATTRIBUTES_INTERRUPT &&
|
||||
it.direction == UsbConstants.USB_DIR_IN
|
||||
}
|
||||
|
||||
val hasOutputEndpoint = usbInterface.endpoints.some {
|
||||
it.attributes and ATTRIBUTES_INTERRUPT_MASK == ATTRIBUTES_INTERRUPT &&
|
||||
it.direction == UsbConstants.USB_DIR_OUT
|
||||
}
|
||||
|
||||
if (hasInputEndpoint && hasOutputEndpoint) {
|
||||
return UsbU2FDevice(
|
||||
device = device,
|
||||
permissionRequestManager = permissionRequestManager,
|
||||
usbManager = usbManager,
|
||||
disconnectReporter = disconnectReporter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun connect(): U2FDeviceSession {
|
||||
if (!permissionRequestManager.requestPermission(device)) {
|
||||
throw U2FException.CommunicationException()
|
||||
}
|
||||
|
||||
val connection = usbManager.openDevice(device)
|
||||
|
||||
try {
|
||||
val descriptors = DeviceDescriptor.parse(connection.rawDescriptors)
|
||||
|
||||
for (configuration in descriptors.configurations) {
|
||||
for (usbInterface in configuration.interfaces) {
|
||||
// same as above
|
||||
if (
|
||||
usbInterface.interfaceClass != CLASS_HID ||
|
||||
usbInterface.interfaceSubclass != 0 ||
|
||||
usbInterface.interfaceProtocol != 0 ||
|
||||
usbInterface.endpoints.size != 2 ||
|
||||
usbInterface.hid == null
|
||||
) continue
|
||||
|
||||
val inputEndpoint = usbInterface.endpoints.find {
|
||||
it.attributes and ATTRIBUTES_INTERRUPT_MASK == ATTRIBUTES_INTERRUPT &&
|
||||
it.address and UsbConstants.USB_ENDPOINT_DIR_MASK == UsbConstants.USB_DIR_IN
|
||||
}
|
||||
|
||||
val outputEndpoint = usbInterface.endpoints.find {
|
||||
it.attributes and ATTRIBUTES_INTERRUPT_MASK == ATTRIBUTES_INTERRUPT &&
|
||||
it.address and UsbConstants.USB_ENDPOINT_DIR_MASK == UsbConstants.USB_DIR_OUT
|
||||
}
|
||||
|
||||
if (inputEndpoint == null || outputEndpoint == null) continue
|
||||
|
||||
val inputEndpointIndex = usbInterface.endpoints.indexOf(inputEndpoint)
|
||||
val outputEndpointIndex = usbInterface.endpoints.indexOf(outputEndpoint)
|
||||
|
||||
val hidDescriptorRaw = U2FThread.usb.executeAndWait {
|
||||
val buffer = ByteArray(usbInterface.hid.reportDescriptorSize)
|
||||
|
||||
if (!connection.claimInterface(
|
||||
device.getInterface(usbInterface.index),
|
||||
true
|
||||
)
|
||||
) {
|
||||
throw U2FException.CommunicationException()
|
||||
}
|
||||
|
||||
val size = connection.controlTransfer(
|
||||
1 /* interface */ or 0x80 /* data from device to host */,
|
||||
6, /* GET_DESCRIPTOR */
|
||||
0x2200, /* first report descriptor (there is always a single one) */
|
||||
usbInterface.index, /* interface index */
|
||||
buffer, /* data */
|
||||
buffer.size, /* length */
|
||||
100 /* timeout */
|
||||
)
|
||||
|
||||
if (size != buffer.size)
|
||||
throw U2FException.CommunicationException()
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
val hidDescriptor = ReportDescriptor.parse(hidDescriptorRaw)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(
|
||||
LOG_TAG,
|
||||
"found device: $hidDescriptor; isU2F = ${hidDescriptor.isU2F}"
|
||||
)
|
||||
}
|
||||
|
||||
if (!hidDescriptor.isU2F) continue
|
||||
|
||||
val u2FConnection = UsbU2FDeviceConnection(
|
||||
inputEndpoint = device.getInterface(usbInterface.index)
|
||||
.getEndpoint(inputEndpointIndex),
|
||||
outputEndpoint = device.getInterface(usbInterface.index)
|
||||
.getEndpoint(outputEndpointIndex),
|
||||
connection = connection,
|
||||
disconnectReporter = disconnectReporter
|
||||
)
|
||||
|
||||
try {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "created connection")
|
||||
}
|
||||
|
||||
u2FConnection.ping(16)
|
||||
u2FConnection.ping(128)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "pings worked")
|
||||
}
|
||||
|
||||
return u2FConnection
|
||||
} catch (ex: Exception) {
|
||||
u2FConnection.cancelPendingRequests()
|
||||
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
break // does not support multiple configurations yet
|
||||
}
|
||||
|
||||
throw U2FException.CommunicationException()
|
||||
} catch (ex: UsbException) {
|
||||
throw U2FException.CommunicationException()
|
||||
} catch (ex: Exception) {
|
||||
connection.close()
|
||||
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
import android.hardware.usb.UsbDeviceConnection
|
||||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbRequest
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.crypto.HexString
|
||||
import io.timelimit.android.u2f.protocol.U2FDeviceSession
|
||||
import io.timelimit.android.u2f.protocol.U2FRequest
|
||||
import io.timelimit.android.u2f.protocol.U2fRawResponse
|
||||
import io.timelimit.android.u2f.util.U2FException
|
||||
import io.timelimit.android.u2f.util.U2FThread
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
class UsbU2FDeviceConnection (
|
||||
private val inputEndpoint: UsbEndpoint,
|
||||
private val outputEndpoint: UsbEndpoint,
|
||||
private val connection: UsbDeviceConnection,
|
||||
private val disconnectReporter: DisconnectReporter
|
||||
): U2FDeviceSession {
|
||||
companion object {
|
||||
private const val LOG_TAG = "UsbU2FDeviceConnection"
|
||||
private const val CHANNEL_BROADCAST = -1
|
||||
private const val CMD_PING = 1.toByte()
|
||||
private const val CMD_MSG = 3.toByte()
|
||||
private const val CMD_INIT = 6.toByte()
|
||||
private const val CMD_ERROR = 0x3f.toByte()
|
||||
private const val TIMEOUT = 3000L
|
||||
}
|
||||
|
||||
internal data class ReceiveRequestClientData(val buffer: ByteBuffer)
|
||||
|
||||
private var channelId: Int? = null
|
||||
private val pendingRequests = mutableListOf<UsbRequest>()
|
||||
|
||||
init { for (i in 0..8) enqueueReceivePacketRequest() }
|
||||
|
||||
fun cancelPendingRequests() {
|
||||
synchronized(pendingRequests) {
|
||||
pendingRequests.forEach { it.close() }
|
||||
pendingRequests.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun enqueueSendPacket(data: ByteArray) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "send packet: ${HexString.toHex(data)}")
|
||||
}
|
||||
|
||||
UsbRequest().also {
|
||||
synchronized(pendingRequests) { pendingRequests.add(it) }
|
||||
|
||||
it.initialize(connection, outputEndpoint)
|
||||
it.queue(ByteBuffer.wrap(data))
|
||||
}
|
||||
}
|
||||
|
||||
private fun enqueueReceivePacketRequest() {
|
||||
UsbRequest().also {
|
||||
synchronized(pendingRequests) { pendingRequests.add(it) }
|
||||
|
||||
val request = ReceiveRequestClientData(ByteBuffer.allocate(inputEndpoint.maxPacketSize))
|
||||
|
||||
it.initialize(connection, inputEndpoint)
|
||||
it.clientData = request
|
||||
it.queue(request.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun receiveResponsePacket(timeout: Long): ByteArray? {
|
||||
val end = SystemClock.uptimeMillis() + timeout
|
||||
|
||||
while (true) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "receiveResponsePacket()")
|
||||
}
|
||||
|
||||
if (disconnectReporter.didDisconnect) throw U2FException.DisconnectedException()
|
||||
|
||||
val remaining = end - SystemClock.uptimeMillis(); if (remaining < 0) break
|
||||
val response = U2FThread.usb.executeAndWait { connection.requestWait(remaining) } ?: continue
|
||||
|
||||
synchronized(pendingRequests) { pendingRequests.remove(response) }
|
||||
|
||||
response.clientData.let { request ->
|
||||
if (request is ReceiveRequestClientData) {
|
||||
enqueueReceivePacketRequest()
|
||||
|
||||
request.buffer.rewind()
|
||||
|
||||
return ByteArray(request.buffer.remaining())
|
||||
.also { request.buffer.get(it) }
|
||||
.also {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got response packet: ${HexString.toHex(it)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun sendRequest(
|
||||
channelId: Int,
|
||||
command: Byte,
|
||||
payload: ByteArray
|
||||
) {
|
||||
if (payload.size >= UShort.MAX_VALUE.toInt()) throw U2FException.CommunicationException()
|
||||
|
||||
val messageSize = outputEndpoint.maxPacketSize; if (messageSize < 8) throw IllegalStateException()
|
||||
val maxPayloadSize = messageSize - 7 + 128 * (messageSize - 5)
|
||||
|
||||
if (payload.size > maxPayloadSize) throw U2FException.CommunicationException()
|
||||
|
||||
var payloadOffset = 0
|
||||
|
||||
kotlin.run { // initial package
|
||||
val buffer = ByteArray(messageSize)
|
||||
|
||||
buffer[0] = channelId.ushr(24).toByte()
|
||||
buffer[1] = channelId.ushr(16).toByte()
|
||||
buffer[2] = channelId.ushr(8).toByte()
|
||||
buffer[3] = channelId.toByte()
|
||||
|
||||
buffer[4] = (command.toInt() or 0x80).toByte() // add init flag
|
||||
|
||||
buffer[5] = payload.size.ushr(8).toByte()
|
||||
buffer[6] = payload.size.toByte()
|
||||
|
||||
payloadOffset = (messageSize - 7).coerceAtMost(payload.size)
|
||||
|
||||
payload.copyInto(
|
||||
destination = buffer,
|
||||
destinationOffset = 7,
|
||||
startIndex = 0,
|
||||
endIndex = payloadOffset
|
||||
)
|
||||
|
||||
enqueueSendPacket(buffer)
|
||||
}
|
||||
|
||||
var sequenceCounter = 0; while (payloadOffset < payload.size) {
|
||||
val buffer = ByteArray(messageSize)
|
||||
|
||||
buffer[0] = channelId.ushr(24).toByte()
|
||||
buffer[1] = channelId.ushr(16).toByte()
|
||||
buffer[2] = channelId.ushr(8).toByte()
|
||||
buffer[3] = channelId.toByte()
|
||||
|
||||
if (sequenceCounter < 0 || sequenceCounter > 0x7f) throw IllegalStateException()
|
||||
buffer[4] = sequenceCounter++.toByte()
|
||||
|
||||
val consumedBytes = (messageSize - 5).coerceAtMost(payload.size - payloadOffset)
|
||||
|
||||
payload.copyInto(
|
||||
destination = buffer,
|
||||
destinationOffset = 5,
|
||||
startIndex = payloadOffset,
|
||||
endIndex = payloadOffset + consumedBytes
|
||||
)
|
||||
|
||||
payloadOffset += consumedBytes
|
||||
|
||||
enqueueSendPacket(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class Response(val channelId: Int, val command: Byte, val payload: ByteArray)
|
||||
|
||||
private suspend fun receiveResponse(timeout: Long): Response? {
|
||||
val end = SystemClock.uptimeMillis() + timeout
|
||||
|
||||
val remaining1 = end - SystemClock.uptimeMillis(); if (remaining1 < 0) return null
|
||||
val response = receiveResponsePacket(remaining1) ?: return null
|
||||
|
||||
if (response.size < 8) throw U2FException.CommunicationException()
|
||||
|
||||
val maxPayloadSize = response.size - 7 + 128 * (response.size - 5)
|
||||
|
||||
val channelId =
|
||||
(response[3].toUByte().toUInt() or
|
||||
response[2].toUByte().toUInt().shl(8) or
|
||||
response[1].toUByte().toUInt().shl(16) or
|
||||
response[0].toUByte().toUInt().shl(24)
|
||||
).toInt()
|
||||
|
||||
val command = response[4].toUByte().toInt()
|
||||
val payloadSize = response[6].toUByte().toInt() or response[5].toUByte().toInt().shl(8)
|
||||
|
||||
if (command and 0x80 != 0x80) return null // not at the start of a reponse
|
||||
if (payloadSize > maxPayloadSize) throw U2FException.CommunicationException()
|
||||
|
||||
val payload = ByteArray(payloadSize)
|
||||
var payloadOffset = (response.size - 7).coerceAtMost(payloadSize)
|
||||
|
||||
response.copyInto(
|
||||
destination = payload,
|
||||
destinationOffset = 0,
|
||||
startIndex = 7,
|
||||
endIndex = response.size.coerceAtMost(payloadSize + 7)
|
||||
)
|
||||
|
||||
var sequenceCounter = 0; while (payloadOffset < payload.size) {
|
||||
val remaining2 = end - SystemClock.uptimeMillis(); if (remaining2 < 0) return null
|
||||
val response2 = receiveResponsePacket(remaining2) ?: return null
|
||||
|
||||
if (response2.size != response.size) throw U2FException.CommunicationException()
|
||||
|
||||
val channelId2 =
|
||||
(response2[3].toUByte().toUInt() or
|
||||
response2[2].toUByte().toUInt().shl(8) or
|
||||
response2[1].toUByte().toUInt().shl(16) or
|
||||
response2[0].toUByte().toUInt().shl(24)
|
||||
).toInt()
|
||||
|
||||
val command2 = response2[4].toUByte().toInt()
|
||||
|
||||
if (sequenceCounter < 0 || sequenceCounter > 0x7f) throw IllegalStateException()
|
||||
val decodedSequenceCounterr = response2[4].toUByte().toInt()
|
||||
|
||||
if (channelId != channelId2) continue
|
||||
if (command2 and 0x80 == 0x80) return null // not at a continuation
|
||||
if (sequenceCounter++ != decodedSequenceCounterr) return null // broken sequence
|
||||
|
||||
val consumedBytes = (response.size - 5).coerceAtMost(payloadSize - payloadOffset)
|
||||
|
||||
response2.copyInto(
|
||||
destination = payload,
|
||||
destinationOffset = payloadOffset,
|
||||
startIndex = 5,
|
||||
endIndex = response.size.coerceAtMost(consumedBytes + 5)
|
||||
)
|
||||
|
||||
payloadOffset += consumedBytes
|
||||
}
|
||||
|
||||
return Response(
|
||||
channelId = channelId,
|
||||
command = (command xor 0x80).toByte(),
|
||||
payload = payload
|
||||
).also {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got response: $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendRequestAndGetResponse(
|
||||
channelId: Int,
|
||||
command: Byte,
|
||||
payload: ByteArray,
|
||||
timeout: Long
|
||||
): Response? {
|
||||
val end = SystemClock.uptimeMillis() + timeout
|
||||
|
||||
sendRequest(channelId = channelId, command = command, payload = payload)
|
||||
|
||||
while (true) {
|
||||
val remaining = end - SystemClock.uptimeMillis(); if (remaining < 0) return null
|
||||
val response = receiveResponse(remaining) ?: continue
|
||||
if (response.channelId != channelId) continue
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun allocateChannelId(): Int {
|
||||
val end = SystemClock.uptimeMillis() + TIMEOUT
|
||||
val nonce = ByteArray(8).also { Random().nextBytes(it) }
|
||||
|
||||
sendRequest(
|
||||
channelId = CHANNEL_BROADCAST,
|
||||
command = CMD_INIT,
|
||||
payload = nonce
|
||||
)
|
||||
|
||||
while (true) {
|
||||
val remaining = end - SystemClock.uptimeMillis(); if (remaining < 0) throw U2FException.CommunicationException()
|
||||
val response = receiveResponse(remaining) ?: continue
|
||||
|
||||
if (response.channelId != CHANNEL_BROADCAST) continue
|
||||
if (response.command != CMD_INIT) continue
|
||||
if (response.payload.size < 17) continue
|
||||
if (!response.payload.sliceArray(0 until 8).contentEquals(nonce)) continue
|
||||
|
||||
val channelId =
|
||||
(response.payload[11].toUByte().toUInt() or
|
||||
response.payload[10].toUByte().toUInt().shl(8) or
|
||||
response.payload[9].toUByte().toUInt().shl(16) or
|
||||
response.payload[8].toUByte().toUInt().shl(24)
|
||||
).toInt()
|
||||
|
||||
if (channelId == CHANNEL_BROADCAST) throw U2FException.CommunicationException()
|
||||
|
||||
return channelId
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOwnChannelId(): Int {
|
||||
channelId?.let { return it }
|
||||
|
||||
allocateChannelId().let { channelId = it; return it }
|
||||
}
|
||||
|
||||
suspend fun ping(payload: ByteArray) {
|
||||
val response = sendRequestAndGetResponse(
|
||||
channelId = getOwnChannelId(),
|
||||
command = CMD_PING,
|
||||
payload = payload,
|
||||
timeout = TIMEOUT
|
||||
) ?: throw U2FException.CommunicationException()
|
||||
|
||||
if (response.command != CMD_PING) throw U2FException.CommunicationException()
|
||||
if (!response.payload.contentEquals(payload)) throw U2FException.CommunicationException()
|
||||
}
|
||||
|
||||
suspend fun ping(length: Int) = ByteArray(length).also {
|
||||
Random().nextBytes(it)
|
||||
|
||||
ping(it)
|
||||
}
|
||||
|
||||
override suspend fun execute(request: U2FRequest): U2fRawResponse {
|
||||
val response = sendRequestAndGetResponse(
|
||||
channelId = getOwnChannelId(),
|
||||
command = CMD_MSG,
|
||||
payload = request.encodeExtended(),
|
||||
timeout = TIMEOUT
|
||||
) ?: throw U2FException.CommunicationException()
|
||||
|
||||
if (response.command != CMD_MSG) throw U2FException.CommunicationException()
|
||||
|
||||
return U2fRawResponse.decode(response.payload).also { it.throwIfNoSuccess() }
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cancelPendingRequests()
|
||||
connection.close()
|
||||
}
|
||||
}
|
117
app/src/main/java/io/timelimit/android/u2f/usb/UsbU2FManager.kt
Normal file
117
app/src/main/java/io/timelimit/android/u2f/usb/UsbU2FManager.kt
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.integration.platform.android.PendingIntentIds
|
||||
import io.timelimit.android.u2f.U2fManager
|
||||
import io.timelimit.android.u2f.util.U2FId
|
||||
|
||||
class UsbU2FManager (val parent: U2fManager, context: Context) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "UsbU2FManager"
|
||||
}
|
||||
|
||||
private val usbManager = context.getSystemService<UsbManager>()
|
||||
private val disconnectReporters = mutableMapOf<String, DisconnectReporter>()
|
||||
|
||||
private val usbConnectionListener = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
try {
|
||||
if (intent.action == UsbManager.ACTION_USB_DEVICE_ATTACHED) handleAddedDevice(intent.usbDevice)
|
||||
else if (intent.action == UsbManager.ACTION_USB_DEVICE_DETACHED) handleRemovedDevice(intent.usbDevice)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "error handling new/removed device", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val permissionResponseAction = U2FId.generate()
|
||||
|
||||
private val permissionResponseListener = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
try {
|
||||
if (intent.action == permissionResponseAction) {
|
||||
val device = intent.usbDevice
|
||||
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got permission $granted for $device")
|
||||
}
|
||||
|
||||
permissionRequestManager.reportResult(device, granted)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "error handling permission response", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val permissionResponseIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
PendingIntentIds.U2F_USB_RESPONSE,
|
||||
Intent(permissionResponseAction),
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
)
|
||||
|
||||
private val permissionRequestManager = UsbPermissionRequestManager(sendRequest = {
|
||||
usbManager?.requestPermission(it, permissionResponseIntent)
|
||||
})
|
||||
|
||||
init {
|
||||
context.registerReceiver(usbConnectionListener, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED))
|
||||
context.registerReceiver(usbConnectionListener, IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED))
|
||||
context.registerReceiver(permissionResponseListener, IntentFilter(permissionResponseAction))
|
||||
}
|
||||
|
||||
private fun handleAddedDevice(device: UsbDevice) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "new device $device")
|
||||
}
|
||||
|
||||
val disconnectReporter = DisconnectReporter().also { disconnectReporters[device.deviceName] = it }
|
||||
|
||||
val u2FDevice = UsbU2FDevice.from(device, permissionRequestManager, usbManager!!, disconnectReporter)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "found u2f device $u2FDevice")
|
||||
}
|
||||
|
||||
u2FDevice?.also { parent.dispatchDeviceFound(it) }
|
||||
}
|
||||
|
||||
private fun handleRemovedDevice(device: UsbDevice) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "disconnected $device")
|
||||
}
|
||||
|
||||
disconnectReporters.remove(device.deviceName)?.reportDisconnect()
|
||||
permissionRequestManager.reportResult(device, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class ConfigurationDescriptor(val interfaces: List<InterfaceDescriptor>) {
|
||||
companion object {
|
||||
const val DESCRIPTOR_TYPE = 2.toByte()
|
||||
|
||||
fun parse(input: ByteArray): Pair<ConfigurationDescriptor, ByteArray> {
|
||||
val descriptorLength = input[0].toUByte().toInt()
|
||||
|
||||
if (descriptorLength < 9 || input.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (input[1] != DESCRIPTOR_TYPE)
|
||||
throw UsbException.InvalidDescriptorTypeException()
|
||||
|
||||
val totalLength = input[2].toUByte().toInt() or input[3].toUByte().toInt().shl(8)
|
||||
|
||||
if (input.size < totalLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
val numInterfaces = input[4].toInt()
|
||||
val interfaces = mutableListOf<InterfaceDescriptor>()
|
||||
|
||||
var remaining = input.sliceArray(descriptorLength until totalLength)
|
||||
|
||||
while (remaining.isNotEmpty()) {
|
||||
if (remaining.size < 2)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
val type = remaining[1]
|
||||
|
||||
if (type == InterfaceDescriptor.DESCRIPTOR_TYPE) {
|
||||
val (newDescriptor, newRemaining) = InterfaceDescriptor.parse(remaining)
|
||||
|
||||
if (newDescriptor.index != interfaces.size)
|
||||
throw UsbException.InvalidIndexException()
|
||||
|
||||
remaining = newRemaining
|
||||
interfaces.add(newDescriptor)
|
||||
} else remaining = UnknownDescriptor.parse(remaining)
|
||||
}
|
||||
|
||||
if (numInterfaces != interfaces.size)
|
||||
throw UsbException.WrongCounterException("interfaces", numInterfaces, interfaces.size)
|
||||
|
||||
return ConfigurationDescriptor(
|
||||
interfaces = interfaces
|
||||
) to input.sliceArray(totalLength until input.size)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class DeviceDescriptor(val configurations: List<ConfigurationDescriptor>) {
|
||||
companion object {
|
||||
private const val DESCRIPTOR_TYPE = 1.toByte()
|
||||
|
||||
fun parse(input: ByteArray): DeviceDescriptor {
|
||||
val descriptorLength = input[0].toUByte().toInt()
|
||||
|
||||
if (descriptorLength < 17 || input.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (input[1] != DESCRIPTOR_TYPE)
|
||||
throw UsbException.InvalidDescriptorTypeException()
|
||||
|
||||
val numConfigurations = input[17].toUByte().toInt()
|
||||
val configurations = mutableListOf<ConfigurationDescriptor>()
|
||||
|
||||
var remaining = input.sliceArray(descriptorLength until input.size)
|
||||
|
||||
while (remaining.isNotEmpty()) {
|
||||
if (remaining.size < 2)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (remaining[1] == ConfigurationDescriptor.DESCRIPTOR_TYPE) {
|
||||
val (newDescriptor, newRemaining) = ConfigurationDescriptor.parse(remaining)
|
||||
|
||||
remaining = newRemaining
|
||||
configurations.add(newDescriptor)
|
||||
} else remaining = UnknownDescriptor.parse(remaining)
|
||||
}
|
||||
|
||||
if (numConfigurations != configurations.size)
|
||||
throw UsbException.WrongCounterException("configuration", numConfigurations, configurations.size)
|
||||
|
||||
return DeviceDescriptor(configurations = configurations)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class EndpointDescriptor(val address: Int, val attributes: Int, val maxPacketSize: Int) {
|
||||
companion object {
|
||||
const val DESCRIPTOR_TYPE = 5.toByte()
|
||||
|
||||
fun parse(input: ByteArray): Pair<EndpointDescriptor, ByteArray> {
|
||||
val descriptorLength = input[0].toUByte().toInt()
|
||||
|
||||
if (descriptorLength < 7 || input.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (input[1] != DESCRIPTOR_TYPE)
|
||||
throw UsbException.InvalidDescriptorTypeException()
|
||||
|
||||
val address = input[2].toUByte().toInt()
|
||||
val attributes = input[3].toUByte().toInt()
|
||||
val maxPacketSize = input[4].toUByte().toInt() or input[5].toUByte().toInt().shl(8)
|
||||
|
||||
return EndpointDescriptor(
|
||||
address = address,
|
||||
attributes = attributes,
|
||||
maxPacketSize = maxPacketSize
|
||||
) to input.sliceArray(descriptorLength until input.size)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class HidDescriptor(val reportDescriptorSize: Int) {
|
||||
companion object {
|
||||
const val DESCRIPTOR_TYPE = 33.toByte()
|
||||
|
||||
fun parse(input: ByteArray): Pair<HidDescriptor, ByteArray> {
|
||||
val descriptorLength = input[0].toUByte().toInt()
|
||||
|
||||
if (descriptorLength < 9 || input.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (input[1] != DESCRIPTOR_TYPE)
|
||||
throw UsbException.InvalidDescriptorTypeException()
|
||||
|
||||
val reportDescriptorSize = input[7].toUByte().toInt() or input[8].toUByte().toInt().shl(8)
|
||||
|
||||
return HidDescriptor(
|
||||
reportDescriptorSize = reportDescriptorSize
|
||||
) to input.sliceArray(descriptorLength until input.size)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class InterfaceDescriptor(
|
||||
val index: Int,
|
||||
val interfaceClass: Int,
|
||||
val interfaceSubclass: Int,
|
||||
val interfaceProtocol: Int,
|
||||
val hid: HidDescriptor?,
|
||||
val endpoints: List<EndpointDescriptor>
|
||||
) {
|
||||
companion object {
|
||||
const val DESCRIPTOR_TYPE = 4.toByte()
|
||||
|
||||
fun parse(input: ByteArray): Pair<InterfaceDescriptor, ByteArray> {
|
||||
val descriptorLength = input[0].toUByte().toInt()
|
||||
|
||||
if (descriptorLength < 9 || input.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
if (input[1] != DESCRIPTOR_TYPE)
|
||||
throw UsbException.InvalidDescriptorTypeException()
|
||||
|
||||
val index = input[2].toUByte().toInt()
|
||||
val numEndpoints = input[4].toUByte().toInt()
|
||||
val interfaceClass = input[5].toUByte().toInt()
|
||||
val interfaceSubclass = input[6].toUByte().toInt()
|
||||
val interfaceProtocol = input[7].toUByte().toInt()
|
||||
|
||||
var hid: HidDescriptor? = null
|
||||
val endpoints = mutableListOf<EndpointDescriptor>()
|
||||
|
||||
var remaining = input.sliceArray(descriptorLength until input.size)
|
||||
|
||||
while (remaining.isNotEmpty()) {
|
||||
if (remaining.size < 2)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
val type = remaining[1]
|
||||
|
||||
if (type == DESCRIPTOR_TYPE) break
|
||||
else if (type == EndpointDescriptor.DESCRIPTOR_TYPE) {
|
||||
val (newDescriptor, newRemaining) = EndpointDescriptor.parse(remaining)
|
||||
|
||||
remaining = newRemaining
|
||||
endpoints.add(newDescriptor)
|
||||
} else if (type == HidDescriptor.DESCRIPTOR_TYPE) {
|
||||
val (newDescriptor, newRemaining) = HidDescriptor.parse(remaining)
|
||||
|
||||
remaining = newRemaining
|
||||
hid = newDescriptor
|
||||
} else remaining = UnknownDescriptor.parse(remaining)
|
||||
}
|
||||
|
||||
if (numEndpoints != endpoints.size)
|
||||
throw UsbException.WrongCounterException("endpoints", numEndpoints, endpoints.size)
|
||||
|
||||
return InterfaceDescriptor(
|
||||
index = index,
|
||||
interfaceClass = interfaceClass,
|
||||
interfaceSubclass = interfaceSubclass,
|
||||
interfaceProtocol = interfaceProtocol,
|
||||
hid = hid,
|
||||
endpoints = endpoints
|
||||
) to remaining
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
data class ReportDescriptor (val items: List<Item>) {
|
||||
companion object {
|
||||
fun parse(input: ByteArray) = ReportDescriptor(Item.parseList(input))
|
||||
|
||||
data class Item(
|
||||
val tag: Int,
|
||||
val type: Int,
|
||||
val data: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
object Type {
|
||||
const val MAIN = 0
|
||||
const val GLOBAL = 1
|
||||
const val LOCAL = 2
|
||||
}
|
||||
|
||||
val usagePageFido = Item(tag = 0, type = Type.GLOBAL, data = byteArrayOf(0xd0.toByte(), 0xf1.toByte()))
|
||||
val usageU2F = Item(tag = 0, type = Type.LOCAL, data = byteArrayOf(1))
|
||||
|
||||
fun parse(input: ByteArray): Pair<Item, ByteArray> {
|
||||
if (input.isEmpty())
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
val baseTag = input[0].toUByte().toInt().ushr(4)
|
||||
val tag: Int
|
||||
val type = input[0].toUByte().toInt().ushr(2) and 3
|
||||
val dataLength: Int
|
||||
val dataOffset: Int
|
||||
|
||||
if (baseTag == 15) {
|
||||
// long item
|
||||
|
||||
if (input.size < 3)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
tag = input[2].toUByte().toInt()
|
||||
dataLength = input[1].toUByte().toInt()
|
||||
dataOffset = 3
|
||||
} else {
|
||||
// regular item
|
||||
tag = baseTag
|
||||
dataLength = when (input[0].toUByte().toInt() and 3) {
|
||||
0 -> 0
|
||||
1 -> 1
|
||||
2 -> 2
|
||||
3 -> 4
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
dataOffset = 1
|
||||
}
|
||||
|
||||
if (input.size < dataOffset + dataLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
val data = input.sliceArray(dataOffset until dataOffset + dataLength)
|
||||
|
||||
return Item(
|
||||
tag = tag,
|
||||
type = type,
|
||||
data = data
|
||||
) to input.sliceArray(dataOffset + dataLength until input.size)
|
||||
}
|
||||
|
||||
fun parseList(input: ByteArray): List<Item> {
|
||||
val result = mutableListOf<Item>()
|
||||
|
||||
var remaining = input
|
||||
|
||||
while (!remaining.isEmpty()) {
|
||||
val (newItem, newRemaining) = parse(remaining)
|
||||
|
||||
remaining = newRemaining
|
||||
result.add(newItem)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Item
|
||||
|
||||
if (tag != other.tag) return false
|
||||
if (type != other.type) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = tag
|
||||
result = 31 * result + type
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isU2F = kotlin.run {
|
||||
val index1 = items.indexOf(Item.usagePageFido)
|
||||
val index2 = items.indexOf(Item.usageU2F)
|
||||
|
||||
-1 < index1 && index1 < index2
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.usb.descriptors
|
||||
|
||||
import io.timelimit.android.u2f.usb.UsbException
|
||||
|
||||
object UnknownDescriptor {
|
||||
fun parse(raw: ByteArray): ByteArray {
|
||||
val descriptorLength = raw[0].toUByte().toInt()
|
||||
|
||||
if (raw.size < descriptorLength)
|
||||
throw UsbException.InvalidDescriptorLengthException()
|
||||
|
||||
return raw.sliceArray(descriptorLength until raw.size)
|
||||
}
|
||||
}
|
|
@ -15,11 +15,12 @@
|
|||
*/
|
||||
package io.timelimit.android.u2f.util
|
||||
|
||||
sealed class U2FException: RuntimeException() {
|
||||
class CommunicationException: U2FException()
|
||||
class DisconnectedException: U2FException()
|
||||
class InvalidDataException: U2FException()
|
||||
class DeviceException: U2FException()
|
||||
class BadKeyHandleException: U2FException()
|
||||
class UserInteractionRequired: U2FException()
|
||||
sealed class U2FException(message: String): RuntimeException(message) {
|
||||
class CommunicationException: U2FException("communication error")
|
||||
class DisconnectedException: U2FException("disconnected error")
|
||||
class InvalidDataException: U2FException("invalid data")
|
||||
class DeviceException(status: UShort): U2FException("device reported error $status")
|
||||
class BadKeyHandleException: U2FException("bad key handle")
|
||||
class UserInteractionRequired: U2FException("user interaction required")
|
||||
class BadRequestLength: U2FException("wrong request length")
|
||||
}
|
25
app/src/main/java/io/timelimit/android/u2f/util/U2FId.kt
Normal file
25
app/src/main/java/io/timelimit/android/u2f/util/U2FId.kt
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.u2f.util
|
||||
|
||||
import io.timelimit.android.crypto.HexString
|
||||
import java.security.SecureRandom
|
||||
|
||||
object U2FId {
|
||||
private val random = SecureRandom()
|
||||
|
||||
fun generate() = ByteArray(32).also { random.nextBytes(it) }.let { HexString.toHex(it) }
|
||||
}
|
|
@ -19,5 +19,6 @@ import java.util.concurrent.Executors
|
|||
|
||||
object U2FThread {
|
||||
val nfc by lazy { Executors.newFixedThreadPool(2) }
|
||||
val usb by lazy { Executors.newFixedThreadPool(2) }
|
||||
val crypto by lazy { Executors.newSingleThreadExecutor() }
|
||||
}
|
|
@ -45,98 +45,99 @@ object AuthTokenLoginProcessor {
|
|||
|
||||
runAsync {
|
||||
try {
|
||||
val session = device.connect()
|
||||
val keys = Threads.database.executeAndWait { model.logic.database.u2f().getAllSync() }
|
||||
val random = SecureRandom()
|
||||
val applicationId = U2FApplicationId.fromUrl(U2FApplicationId.URL)
|
||||
|
||||
for (key in keys) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "try key $key")
|
||||
}
|
||||
|
||||
val challenge = ByteArray(32).also { random.nextBytes(it) }
|
||||
|
||||
try {
|
||||
val response = session.login(
|
||||
U2FRequest.Login(
|
||||
mode = U2FRequest.Login.Mode.DoNotEnforcePresence,
|
||||
challenge = challenge,
|
||||
applicationId = applicationId,
|
||||
keyHandle = key.keyHandle
|
||||
)
|
||||
)
|
||||
device.connect().use { session ->
|
||||
val keys = Threads.database.executeAndWait { model.logic.database.u2f().getAllSync() }
|
||||
val random = SecureRandom()
|
||||
val applicationId = U2FApplicationId.fromUrl(U2FApplicationId.URL)
|
||||
|
||||
for (key in keys) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got response $response")
|
||||
Log.d(LOG_TAG, "try key $key")
|
||||
}
|
||||
|
||||
val signatureValid = U2FThread.crypto.executeAndWait {
|
||||
U2FSignatureValidation.validate(
|
||||
applicationId = applicationId,
|
||||
challenge = challenge,
|
||||
response = response,
|
||||
publicKey = key.publicKey
|
||||
val challenge = ByteArray(32).also { random.nextBytes(it) }
|
||||
|
||||
try {
|
||||
val response = session.login(
|
||||
U2FRequest.Login(
|
||||
mode = U2FRequest.Login.Mode.DoNotEnforcePresence,
|
||||
challenge = challenge,
|
||||
applicationId = applicationId,
|
||||
keyHandle = key.keyHandle
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!signatureValid) {
|
||||
toast(R.string.u2f_login_error_invalid)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got response $response")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
val signatureValid = U2FThread.crypto.executeAndWait {
|
||||
U2FSignatureValidation.validate(
|
||||
applicationId = applicationId,
|
||||
challenge = challenge,
|
||||
response = response,
|
||||
publicKey = key.publicKey
|
||||
)
|
||||
}
|
||||
|
||||
val userEntry = Threads.database.executeAndWait {
|
||||
if (
|
||||
model.logic.database.u2f().updateCounter(
|
||||
parentUserId = key.userId,
|
||||
keyHandle = key.keyHandle,
|
||||
publicKey = key.publicKey,
|
||||
counter = response.counter.toLong()
|
||||
) > 0
|
||||
) {
|
||||
model.logic.database.user().getUserByIdSync(key.userId)!!
|
||||
} else null
|
||||
}
|
||||
if (!signatureValid) {
|
||||
toast(R.string.u2f_login_error_invalid)
|
||||
|
||||
if (userEntry == null) {
|
||||
toast(R.string.u2f_login_error_invalid)
|
||||
break
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
val userEntry = Threads.database.executeAndWait {
|
||||
if (
|
||||
model.logic.database.u2f().updateCounter(
|
||||
parentUserId = key.userId,
|
||||
keyHandle = key.keyHandle,
|
||||
publicKey = key.publicKey,
|
||||
counter = response.counter.toLong()
|
||||
) > 0
|
||||
) {
|
||||
model.logic.database.user().getUserByIdSync(key.userId)!!
|
||||
} else null
|
||||
}
|
||||
|
||||
val allowLoginStatus = Threads.database.executeAndWait {
|
||||
AllowUserLoginStatusUtil.calculateSync(
|
||||
logic = model.logic,
|
||||
userId = userEntry.id,
|
||||
didSync = false
|
||||
if (userEntry == null) {
|
||||
toast(R.string.u2f_login_error_invalid)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
val allowLoginStatus = Threads.database.executeAndWait {
|
||||
AllowUserLoginStatusUtil.calculateSync(
|
||||
logic = model.logic,
|
||||
userId = userEntry.id,
|
||||
didSync = false
|
||||
)
|
||||
}
|
||||
|
||||
val shouldSignIn = allowLoginStatus is AllowUserLoginStatus.Allow
|
||||
|
||||
if (!shouldSignIn) {
|
||||
toast(LoginDialogFragmentModel.formatAllowLoginStatusError(allowLoginStatus, model.getApplication()))
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
model.setAuthenticatedUser(
|
||||
AuthenticatedUser(
|
||||
userId = key.userId,
|
||||
authenticatedBy = AuthenticationMethod.KeyCode,
|
||||
firstPasswordHash = userEntry.password,
|
||||
secondPasswordHash = "u2f"
|
||||
)
|
||||
)
|
||||
|
||||
return@runAsync // no need to try more
|
||||
} catch (ex: U2FException.BadKeyHandleException) {
|
||||
// ignore and try the next one
|
||||
}
|
||||
|
||||
val shouldSignIn = allowLoginStatus is AllowUserLoginStatus.Allow
|
||||
|
||||
if (!shouldSignIn) {
|
||||
toast(LoginDialogFragmentModel.formatAllowLoginStatusError(allowLoginStatus, model.getApplication()))
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
model.setAuthenticatedUser(
|
||||
AuthenticatedUser(
|
||||
userId = key.userId,
|
||||
authenticatedBy = AuthenticationMethod.KeyCode,
|
||||
firstPasswordHash = userEntry.password,
|
||||
secondPasswordHash = "u2f"
|
||||
)
|
||||
)
|
||||
|
||||
return@runAsync // no need to try more
|
||||
} catch (ex: U2FException.BadKeyHandleException) {
|
||||
// ignore and try the next one
|
||||
}
|
||||
}
|
||||
|
||||
toast(R.string.u2f_login_error_unknown)
|
||||
toast(R.string.u2f_login_error_unknown)
|
||||
}
|
||||
} catch (ex: U2FException.DisconnectedException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "disconnected", ex)
|
||||
|
|
|
@ -35,6 +35,8 @@ class Adapter() : RecyclerView.Adapter<Adapter.Holder>() {
|
|||
var items: List<U2FKeyListItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var listener: Handlers? = null
|
||||
|
||||
init { setHasStableIds(true) }
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder = when (viewType) {
|
||||
TYPE_ADD -> Holder.Add(AddItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)).also {
|
||||
it.binding.label = parent.context.getString(R.string.manage_parent_u2f_add_key)
|
||||
|
|
|
@ -71,6 +71,7 @@ class AddU2FDialogFragment: BottomSheetDialogFragment() {
|
|||
AddU2FModel.Status.ConnectionInterrupted -> getString(R.string.manage_parent_u2f_status_interrupted)
|
||||
AddU2FModel.Status.RequestFailed -> getString(R.string.manage_parent_u2f_status_failed)
|
||||
AddU2FModel.Status.AlreadyLinked -> getString(R.string.manage_parent_u2f_status_already_linked)
|
||||
AddU2FModel.Status.NeedsUserInteraction -> getString(R.string.manage_parent_u2f_status_needs_user_interaction)
|
||||
is AddU2FModel.Status.Done -> {
|
||||
if (!status.commited) {
|
||||
activityModel.tryDispatchParentAction(status.action)
|
||||
|
|
|
@ -33,6 +33,7 @@ import io.timelimit.android.u2f.protocol.U2FRequest
|
|||
import io.timelimit.android.u2f.protocol.login
|
||||
import io.timelimit.android.u2f.protocol.register
|
||||
import io.timelimit.android.u2f.util.U2FException
|
||||
import kotlinx.coroutines.delay
|
||||
import java.security.SecureRandom
|
||||
|
||||
class AddU2FModel(application: Application): AndroidViewModel(application), U2fManager.DeviceFoundListener {
|
||||
|
@ -59,36 +60,56 @@ class AddU2FModel(application: Application): AndroidViewModel(application), U2fM
|
|||
try {
|
||||
val applicationId = U2FApplicationId.fromUrl(U2FApplicationId.URL)
|
||||
|
||||
val session = device.connect()
|
||||
device.connect().use { session ->
|
||||
val currentKeys = Threads.database.executeAndWait {logic.database.u2f().getAllSync() }
|
||||
|
||||
val currentKeys = Threads.database.executeAndWait { logic.database.u2f().getAllSync() }
|
||||
|
||||
for (key in currentKeys) {
|
||||
try {
|
||||
session.login(
|
||||
U2FRequest.Login(
|
||||
mode = U2FRequest.Login.Mode.CheckOnly,
|
||||
challenge = ByteArray(32).also { SecureRandom().nextBytes(it) },
|
||||
applicationId = applicationId,
|
||||
keyHandle = key.keyHandle
|
||||
for (key in currentKeys) {
|
||||
try {
|
||||
session.login(
|
||||
U2FRequest.Login(
|
||||
mode = U2FRequest.Login.Mode.CheckOnly,
|
||||
challenge = ByteArray(32).also { SecureRandom().nextBytes(it) },
|
||||
applicationId = applicationId,
|
||||
keyHandle = key.keyHandle
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (ex: U2FException.BadKeyHandleException) {
|
||||
continue
|
||||
} catch (ex: U2FException.UserInteractionRequired) {
|
||||
statusInternal.value = Status.AlreadyLinked
|
||||
} catch (ex: U2FException.BadKeyHandleException) {
|
||||
continue
|
||||
} catch (ex: U2FException.UserInteractionRequired) {
|
||||
statusInternal.value = Status.AlreadyLinked
|
||||
|
||||
return@runAsync
|
||||
return@runAsync
|
||||
}
|
||||
}
|
||||
|
||||
val challenge = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
val registerResponse = session.register(
|
||||
U2FRequest.Register(
|
||||
challenge = challenge,
|
||||
applicationId = applicationId
|
||||
)
|
||||
)
|
||||
|
||||
statusInternal.value = Status.Done(
|
||||
AddParentU2FKey(
|
||||
keyHandle = registerResponse.keyHandle,
|
||||
publicKey = registerResponse.publicKey
|
||||
)
|
||||
)
|
||||
|
||||
break
|
||||
} catch (ex: U2FException.UserInteractionRequired) {
|
||||
if (statusInternal.value != Status.NeedsUserInteraction) {
|
||||
statusInternal.value = Status.NeedsUserInteraction
|
||||
}
|
||||
|
||||
delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val challenge = ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||
|
||||
val registerResponse = session.register(U2FRequest.Register(challenge = challenge, applicationId = applicationId))
|
||||
|
||||
statusInternal.value = Status.Done(
|
||||
AddParentU2FKey(keyHandle = registerResponse.keyHandle, publicKey = registerResponse.publicKey)
|
||||
)
|
||||
} catch (ex: U2FException.DisconnectedException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "connection interrupted", ex)
|
||||
|
@ -111,6 +132,7 @@ class AddU2FModel(application: Application): AndroidViewModel(application), U2fM
|
|||
object ConnectionInterrupted: Status()
|
||||
object RequestFailed: Status()
|
||||
object AlreadyLinked: Status()
|
||||
object NeedsUserInteraction: Status()
|
||||
data class Done(val action: AddParentU2FKey, var commited: Boolean = false): Status()
|
||||
}
|
||||
}
|
|
@ -1027,7 +1027,7 @@
|
|||
<string name="manage_parent_change_password_toast_wrong_password">Das alte Passwort ist falsch</string>
|
||||
|
||||
<string name="manage_parent_u2f_title">U2F-Schlüssel</string>
|
||||
<string name="manage_parent_u2f_description">NFC U2F-Tokens zum Authentifizieren einrichten</string>
|
||||
<string name="manage_parent_u2f_description">U2F-Tokens (über USB oder NFC) zum Authentifizieren einrichten</string>
|
||||
|
||||
<string name="manage_parent_restore_password_text_require_mail">
|
||||
Dies ermöglicht es, ein neues Passwort festzulegen, ohne das alte Passwort zu kennen.
|
||||
|
@ -1657,7 +1657,7 @@
|
|||
<string name="notify_permission_rejected_toast">abgelehnt; möglicherweise können Sie dies nur noch über die Systemeinstellungen erlauben</string>
|
||||
|
||||
<string name="manage_parent_u2f_add_key">Token hinzufügen</string>
|
||||
<string name="manage_parent_u2f_status_wait_key">Warte auf einen Schlüssel; NFC %s</string>
|
||||
<string name="manage_parent_u2f_status_wait_key">Warte auf einen Schlüssel; Verbinden Sie jetzt den Schlüssel; NFC %s</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_enabled">ist aktiviert</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_disabled">ist deaktiviert</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_unsupported">wird von Ihrem Gerät nicht unterstützt</string>
|
||||
|
@ -1665,6 +1665,7 @@
|
|||
<string name="manage_parent_u2f_status_interrupted">Verbindung unterbrochen</string>
|
||||
<string name="manage_parent_u2f_status_failed">Anfrage fehlgeschlagen</string>
|
||||
<string name="manage_parent_u2f_status_already_linked">Dieses Gerät ist bereits verknüpft</string>
|
||||
<string name="manage_parent_u2f_status_needs_user_interaction">Warte auf Druck der Bestätigen-Taste am Schlüssel</string>
|
||||
<string name="manage_parent_u2f_status_done">Das Gerät wurde verknüpft</string>
|
||||
|
||||
<string name="manage_parent_u2f_remove_key_text">Möchten sich diesen Schlüssel wirklich entfernen?</string>
|
||||
|
|
|
@ -1075,7 +1075,7 @@
|
|||
<string name="manage_parent_change_password_toast_wrong_password">The old password is invalid</string>
|
||||
|
||||
<string name="manage_parent_u2f_title">U2F Keys</string>
|
||||
<string name="manage_parent_u2f_description">Setup NFC U2F Tokens to authenticate</string>
|
||||
<string name="manage_parent_u2f_description">Setup U2F Tokens (using USB or NFC) to authenticate</string>
|
||||
|
||||
<string name="manage_parent_restore_password_title" translatable="false">@string/restore_parent_password_title</string>
|
||||
<string name="manage_parent_restore_password_text_require_mail">
|
||||
|
@ -1709,7 +1709,7 @@
|
|||
<string name="notify_permission_rejected_toast">rejected; it could be that you can only change this using the system settings</string>
|
||||
|
||||
<string name="manage_parent_u2f_add_key">Add Token</string>
|
||||
<string name="manage_parent_u2f_status_wait_key">Waiting for your key; NFC is %s</string>
|
||||
<string name="manage_parent_u2f_status_wait_key">Waiting for your key; Connect the key now; NFC is %s</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_enabled">enabled</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_disabled">disabled</string>
|
||||
<string name="manage_parent_u2f_status_wait_key_nfc_unsupported">unsupported</string>
|
||||
|
@ -1717,6 +1717,7 @@
|
|||
<string name="manage_parent_u2f_status_interrupted">Connection was interrupted</string>
|
||||
<string name="manage_parent_u2f_status_failed">The Request Failed</string>
|
||||
<string name="manage_parent_u2f_status_already_linked">This device is already linked</string>
|
||||
<string name="manage_parent_u2f_status_needs_user_interaction">Waiting for button press at the key</string>
|
||||
<string name="manage_parent_u2f_status_done">The Device was linked</string>
|
||||
|
||||
<string name="manage_parent_u2f_remove_key_text">Would you like to remove this key?</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue