mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-06 03:50:23 +02:00
Add U2F support to the connected mode
This commit is contained in:
parent
f804f421ff
commit
d8b492e2e3
26 changed files with 505 additions and 173 deletions
|
@ -27,7 +27,10 @@ import javax.crypto.KeyAgreement
|
|||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
data class DHHandshake(val keyVersion: String, val ownPublicKey: ByteArray, val sharedSecret: ByteArray) {
|
||||
data class DHHandshake(
|
||||
val keyVersion: String, val otherPublicKey: ByteArray,
|
||||
val ownPublicKey: ByteArray, val sharedSecret: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
private const val SHARED_SECRET_LENGTH = 32
|
||||
private const val AES_KEY_SIZE = 16
|
||||
|
@ -51,6 +54,7 @@ data class DHHandshake(val keyVersion: String, val ownPublicKey: ByteArray, val
|
|||
|
||||
return DHHandshake(
|
||||
keyVersion = serverDhKey.version,
|
||||
otherPublicKey = serverDhKey.key,
|
||||
ownPublicKey = ownPublicKey,
|
||||
sharedSecret = sharedSecret
|
||||
)
|
||||
|
|
|
@ -379,4 +379,12 @@ abstract class ConfigDao {
|
|||
updateValueSync(ConfigurationItemType.DhKeyVersion, key.version)
|
||||
updateValueSync(ConfigurationItemType.DhKey, key.key.base64())
|
||||
}
|
||||
|
||||
fun getU2fVersionSync(): String? {
|
||||
return getValueOfKeySync(ConfigurationItemType.U2fListVersion)
|
||||
}
|
||||
|
||||
fun setU2fListVersionSync(version: String?) {
|
||||
updateValueSync(ConfigurationItemType.U2fListVersion, version)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,9 @@ interface U2FDao {
|
|||
@Query("SELECT * FROM user_u2f_key WHERE user_id = :userId")
|
||||
fun getByUserLive(userId: String): LiveData<List<UserU2FKey>>
|
||||
|
||||
@Query("SELECT * FROM user_u2f_key WHERE key_id = :keyId")
|
||||
fun getByClientKeyIdLive(keyId: Long): LiveData<UserU2FKey?>
|
||||
|
||||
@Query("UPDATe user_u2f_key SET next_counter = :counter + 1 WHERE user_id = :parentUserId AND key_handle = :keyHandle AND public_key = :publicKey AND :counter >= next_counter")
|
||||
fun updateCounter(parentUserId: String, keyHandle: ByteArray, publicKey: ByteArray, counter: Long): Int
|
||||
}
|
|
@ -105,7 +105,8 @@ enum class ConfigurationItemType {
|
|||
LastServerKeyRequestSequence,
|
||||
LastKeyResponseSequence,
|
||||
DhKey,
|
||||
DhKeyVersion
|
||||
DhKeyVersion,
|
||||
U2fListVersion
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
|
@ -140,6 +141,7 @@ object ConfigurationItemTypeUtil {
|
|||
private const val LAST_SERVER_KEY_RESPONSE_SEQUENCE = 30
|
||||
private const val DH_KEY = 31
|
||||
private const val DH_KEY_VERSION = 32
|
||||
private const val U2F_LIST_VERSION = 33
|
||||
|
||||
val TYPES = listOf(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
|
@ -172,7 +174,8 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.LastServerKeyRequestSequence,
|
||||
ConfigurationItemType.LastKeyResponseSequence,
|
||||
ConfigurationItemType.DhKey,
|
||||
ConfigurationItemType.DhKeyVersion
|
||||
ConfigurationItemType.DhKeyVersion,
|
||||
ConfigurationItemType.U2fListVersion
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
|
@ -207,6 +210,7 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.LastKeyResponseSequence -> LAST_SERVER_KEY_RESPONSE_SEQUENCE
|
||||
ConfigurationItemType.DhKey -> DH_KEY
|
||||
ConfigurationItemType.DhKeyVersion -> DH_KEY_VERSION
|
||||
ConfigurationItemType.U2fListVersion -> U2F_LIST_VERSION
|
||||
}
|
||||
|
||||
fun parse(value: Int) = when(value) {
|
||||
|
@ -241,6 +245,7 @@ object ConfigurationItemTypeUtil {
|
|||
LAST_SERVER_KEY_RESPONSE_SEQUENCE -> ConfigurationItemType.LastKeyResponseSequence
|
||||
DH_KEY -> ConfigurationItemType.DhKey
|
||||
DH_KEY_VERSION -> ConfigurationItemType.DhKeyVersion
|
||||
U2F_LIST_VERSION -> ConfigurationItemType.U2fListVersion
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
package io.timelimit.android.data.model
|
||||
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.extensions.base64
|
||||
import io.timelimit.android.extensions.toByteArray
|
||||
import java.security.MessageDigest
|
||||
|
||||
@Entity(
|
||||
tableName = "user_u2f_key",
|
||||
|
@ -49,4 +52,14 @@ data class UserU2FKey (
|
|||
val publicKey: ByteArray,
|
||||
@ColumnInfo(name = "next_counter")
|
||||
val nextCounter: Long
|
||||
)
|
||||
) {
|
||||
fun calculateServerKeyIdSync() = MessageDigest.getInstance("SHA256").also {
|
||||
it.update(keyHandle.size.toByteArray())
|
||||
it.update(keyHandle)
|
||||
|
||||
it.update(publicKey.size.toByteArray())
|
||||
it.update(publicKey)
|
||||
}.digest()
|
||||
.sliceArray(0 until 6)
|
||||
.base64()
|
||||
}
|
29
app/src/main/java/io/timelimit/android/extensions/Numbers.kt
Normal file
29
app/src/main/java/io/timelimit/android/extensions/Numbers.kt
Normal file
|
@ -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.extensions
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
fun Int.toByteArray() = ByteBuffer.allocate(Int.SIZE_BYTES).also {
|
||||
it.putInt(this)
|
||||
it.rewind()
|
||||
}.toByteString().toByteArray()
|
||||
|
||||
fun Long.toByteArray() = ByteBuffer.allocate(Long.SIZE_BYTES).also {
|
||||
it.putLong(this)
|
||||
it.rewind()
|
||||
}.toByteString().toByteArray()
|
|
@ -614,6 +614,42 @@ object ApplyServerDataStatus {
|
|||
}
|
||||
}
|
||||
|
||||
status.u2f?.also { u2f ->
|
||||
database.config().setU2fListVersionSync(u2f.version)
|
||||
|
||||
val savedKeys = database.u2f().getAllSync().toMutableList()
|
||||
|
||||
u2f.data.forEach { newKey ->
|
||||
val oldKey = savedKeys.find {
|
||||
it.keyHandle.contentEquals(newKey.keyHandle) &&
|
||||
it.publicKey.contentEquals(newKey.publicKey)
|
||||
}
|
||||
|
||||
if (oldKey != null) {
|
||||
savedKeys.remove(oldKey)
|
||||
} else {
|
||||
database.u2f().addKey(
|
||||
UserU2FKey(
|
||||
keyId = 0,
|
||||
userId = newKey.userId,
|
||||
addedAt = newKey.addedAt,
|
||||
keyHandle = newKey.keyHandle,
|
||||
publicKey = newKey.publicKey,
|
||||
nextCounter = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
savedKeys.forEach { key ->
|
||||
database.u2f().deleteKey(
|
||||
parentUserId = key.userId,
|
||||
keyHandle = key.keyHandle,
|
||||
publicKey = key.publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Result(
|
||||
newDeviceTitles = newDeviceTitles,
|
||||
didCreateNewActions = didCreateNewActions
|
||||
|
|
|
@ -2122,6 +2122,18 @@ data class RemoveParentU2FKey(val publicKey: ByteArray, val keyHandle: ByteArray
|
|||
}
|
||||
}
|
||||
|
||||
object ReportU2fLoginAction: ParentAction() {
|
||||
const val TYPE_VALUE = "REPORT_U2F_LOGIN"
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
// child actions
|
||||
object ChildSignInAction: ChildAction() {
|
||||
private const val TYPE_VALUE = "CHILD_SIGN_IN"
|
||||
|
|
|
@ -25,15 +25,21 @@ import io.timelimit.android.data.Database
|
|||
import io.timelimit.android.data.model.PendingSyncAction
|
||||
import io.timelimit.android.data.model.PendingSyncActionType
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.extensions.base64
|
||||
import io.timelimit.android.extensions.toByteArray
|
||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.ServerApiLevelLogic
|
||||
import io.timelimit.android.sync.SyncUtil
|
||||
import io.timelimit.android.sync.actions.*
|
||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseAppLogicActionDispatcher
|
||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseChildActionDispatcher
|
||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import org.json.JSONObject
|
||||
import java.io.StringWriter
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object ApplyActionUtil {
|
||||
private const val LOG_TAG = "ApplyActionUtil"
|
||||
|
@ -192,7 +198,13 @@ object ApplyActionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun applyParentAction(action: ParentAction, database: Database, authentication: ApplyActionParentAuthentication, syncUtil: SyncUtil, platformIntegration: PlatformIntegration) {
|
||||
suspend fun applyParentAction(
|
||||
action: ParentAction,
|
||||
database: Database,
|
||||
authentication: ApplyActionParentAuthentication,
|
||||
syncUtil: SyncUtil,
|
||||
platformIntegration: PlatformIntegration
|
||||
) {
|
||||
Threads.database.executeAndWait {
|
||||
database.runInTransaction {
|
||||
val deviceUserIdBeforeDispatchingForDeviceAuth = if (authentication is ApplyActionParentDeviceAuthentication || authentication is ApplyActionChildAddLimitAuthentication) {
|
||||
|
@ -226,7 +238,7 @@ object ApplyActionUtil {
|
|||
database = database,
|
||||
fromChildSelfLimitAddChildUserId = if (authentication is ApplyActionChildAddLimitAuthentication) deviceUserIdBeforeDispatchingForDeviceAuth else null,
|
||||
parentUserId = when (authentication) {
|
||||
is ApplyActionParentPasswordAuthentication -> authentication.parentUserId
|
||||
is ApplyActionUserAuthentication -> authentication.user.userId
|
||||
is ApplyActionParentDeviceAuthentication -> deviceUserIdBeforeDispatchingForDeviceAuth
|
||||
is ApplyActionChildAddLimitAuthentication -> null
|
||||
}
|
||||
|
@ -249,27 +261,62 @@ object ApplyActionUtil {
|
|||
|
||||
val sequenceNumber = database.config().getNextSyncActionSequenceActionAndIncrementIt()
|
||||
|
||||
fun mac(secret: ByteArray) = Mac.getInstance("HmacSHA256").also {
|
||||
val binaryDeviceId = database.config().getOwnDeviceIdSync()!!.toByteArray(Charsets.UTF_8)
|
||||
val binaryAction = serializedAction.toByteArray(Charsets.UTF_8)
|
||||
|
||||
it.init(SecretKeySpec(secret, "HmacSHA256"))
|
||||
|
||||
it.update(sequenceNumber.toByteArray())
|
||||
|
||||
it.update(binaryDeviceId.size.toByteArray())
|
||||
it.update(binaryDeviceId)
|
||||
|
||||
it.update(binaryAction.size.toByteArray())
|
||||
it.update(binaryAction)
|
||||
}.doFinal()
|
||||
|
||||
val pendingAction = when (authentication) {
|
||||
is ApplyActionParentPasswordAuthentication -> {
|
||||
val integrityData = sequenceNumber.toString() +
|
||||
database.config().getOwnDeviceIdSync() +
|
||||
authentication.secondPasswordHash +
|
||||
serializedAction
|
||||
is ApplyActionUserAuthentication -> {
|
||||
val integrity = when (authentication.user) {
|
||||
is AuthenticatedUser.Password -> {
|
||||
val serverLevel = ServerApiLevelLogic.getSync(database)
|
||||
|
||||
val hashedIntegrityData = Sha512.hashSync(integrityData)
|
||||
if (serverLevel.hasLevelOrIsOffline(6)) {
|
||||
val mac = mac(authentication.user.secondPasswordHash.toByteArray(Charsets.UTF_8))
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "integrity data: $integrityData")
|
||||
Log.d(LOG_TAG, "integrity hash: $hashedIntegrityData")
|
||||
"password:${mac.base64()}"
|
||||
} else {
|
||||
val integrityData = sequenceNumber.toString() +
|
||||
database.config().getOwnDeviceIdSync() +
|
||||
authentication.user.secondPasswordHash +
|
||||
serializedAction
|
||||
|
||||
val hashedIntegrityData = Sha512.hashSync(integrityData)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "integrity data: $integrityData")
|
||||
Log.d(LOG_TAG, "integrity hash: $hashedIntegrityData")
|
||||
}
|
||||
|
||||
hashedIntegrityData
|
||||
}
|
||||
}
|
||||
is AuthenticatedUser.U2fSigned -> {
|
||||
val mac = mac(authentication.user.dh.sharedSecret)
|
||||
|
||||
constructU2fIntegrityString(authentication.user, mac)
|
||||
}
|
||||
is AuthenticatedUser.LocalAuth -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
PendingSyncAction(
|
||||
sequenceNumber = sequenceNumber,
|
||||
encodedAction = serializedAction,
|
||||
integrity = hashedIntegrityData,
|
||||
scheduledForUpload = false,
|
||||
type = PendingSyncActionType.Parent,
|
||||
userId = authentication.parentUserId
|
||||
sequenceNumber = sequenceNumber,
|
||||
encodedAction = serializedAction,
|
||||
integrity = integrity,
|
||||
scheduledForUpload = false,
|
||||
type = PendingSyncActionType.Parent,
|
||||
userId = authentication.user.userId
|
||||
)
|
||||
}
|
||||
ApplyActionParentDeviceAuthentication -> {
|
||||
|
@ -353,6 +400,10 @@ object ApplyActionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
fun constructU2fIntegrityString(authentication: AuthenticatedUser.U2fSigned, mac: ByteArray): String {
|
||||
return "u2f:${authentication.dh.keyVersion}.${authentication.dh.ownPublicKey.base64()}.${authentication.u2fServerKeyId}.${authentication.signature.raw.base64()}.${mac.base64()}"
|
||||
}
|
||||
|
||||
private fun isSyncEnabled(database: Database): Boolean {
|
||||
return database.config().getDeviceAuthTokenSync() != ""
|
||||
}
|
||||
|
@ -361,6 +412,34 @@ object ApplyActionUtil {
|
|||
sealed class ApplyActionParentAuthentication
|
||||
object ApplyActionParentDeviceAuthentication: ApplyActionParentAuthentication()
|
||||
object ApplyActionChildAddLimitAuthentication: ApplyActionParentAuthentication()
|
||||
data class ApplyActionParentPasswordAuthentication(val parentUserId: String, val secondPasswordHash: String): ApplyActionParentAuthentication()
|
||||
|
||||
data class ApplyActionUserAuthentication(val user: AuthenticatedUser): ApplyActionParentAuthentication()
|
||||
data class ApplyActionChildAuthentication(val childUserId: String, val secondPasswordHash: String)
|
||||
|
||||
data class ApplyDirectCallAuthentication (
|
||||
val parentUserId: String,
|
||||
val parentPasswordSecondHash: String
|
||||
) {
|
||||
companion object {
|
||||
fun from(auth: ApplyActionParentAuthentication) = when (auth) {
|
||||
ApplyActionParentDeviceAuthentication -> ApplyDirectCallAuthentication(
|
||||
parentUserId = "",
|
||||
parentPasswordSecondHash = "device"
|
||||
)
|
||||
is ApplyActionUserAuthentication -> ApplyDirectCallAuthentication(
|
||||
parentUserId = auth.user.userId,
|
||||
parentPasswordSecondHash = when (auth.user) {
|
||||
is AuthenticatedUser.Password -> auth.user.secondPasswordHash
|
||||
is AuthenticatedUser.U2fSigned -> ApplyActionUtil.constructU2fIntegrityString(
|
||||
auth.user,
|
||||
Mac.getInstance("HmacSHA256").also {
|
||||
it.init(SecretKeySpec(auth.user.dh.sharedSecret, "HmacSHA256"))
|
||||
it.update("direct action".toByteArray(Charsets.UTF_8))
|
||||
}.doFinal()
|
||||
)
|
||||
is AuthenticatedUser.LocalAuth -> throw RuntimeException("authentication does not support that")
|
||||
}
|
||||
)
|
||||
is ApplyActionChildAddLimitAuthentication -> throw RuntimeException("child can not do that")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -30,6 +30,7 @@ class UploadActionsUtil(private val database: Database, private val syncConflict
|
|||
database.runInTransaction {
|
||||
database.config().setUserListVersionSync("")
|
||||
database.config().setDeviceListVersionSync("")
|
||||
database.config().setU2fListVersionSync(null)
|
||||
database.device().deleteAllInstalledAppsVersions()
|
||||
database.category().deleteAllCategoriesVersionNumbers()
|
||||
database.cryptContainer().deleteAllServerVersionNumbers()
|
||||
|
|
|
@ -922,6 +922,7 @@ object LocalDatabaseParentActionDispatcher {
|
|||
publicKey = action.publicKey
|
||||
)
|
||||
}
|
||||
is ReportU2fLoginAction -> null // nothing to do
|
||||
}.let { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ data class ClientDataStatus(
|
|||
val userListVersion: String,
|
||||
val lastKeyRequestServerSequence: Long?,
|
||||
val lastKeyResponseServerSequence: Long?,
|
||||
val dhKeyVersion: String?
|
||||
val dhKeyVersion: String?,
|
||||
val u2fVersion: String?
|
||||
) {
|
||||
companion object {
|
||||
private const val DEVICES = "devices"
|
||||
|
@ -38,7 +39,8 @@ data class ClientDataStatus(
|
|||
private const val LAST_KEY_REQUEST_SEQUENCE = "kri"
|
||||
private const val LAST_KEY_RESPONSE_SEQUENCE = "kr"
|
||||
private const val DH = "dh"
|
||||
private const val CLIENT_LEVEL_VALUE = 5
|
||||
private const val U2F = "u2f"
|
||||
private const val CLIENT_LEVEL_VALUE = 6
|
||||
|
||||
val empty = ClientDataStatus(
|
||||
deviceListVersion = "",
|
||||
|
@ -48,7 +50,8 @@ data class ClientDataStatus(
|
|||
userListVersion = "",
|
||||
lastKeyRequestServerSequence = null,
|
||||
lastKeyResponseServerSequence = null,
|
||||
dhKeyVersion = null
|
||||
dhKeyVersion = null,
|
||||
u2fVersion = null
|
||||
)
|
||||
|
||||
fun getClientDataStatusSync(database: Database): ClientDataStatus {
|
||||
|
@ -87,7 +90,8 @@ data class ClientDataStatus(
|
|||
userListVersion = database.config().getUserListVersionSync(),
|
||||
lastKeyRequestServerSequence = database.config().getLastServerKeyRequestSequenceSync(),
|
||||
lastKeyResponseServerSequence = database.config().getLastServerKeyResponseSequenceSync(),
|
||||
dhKeyVersion = database.config().getLastDhKeySync()?.version
|
||||
dhKeyVersion = database.config().getLastDhKeySync()?.version,
|
||||
u2fVersion = database.config().getU2fVersionSync()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +132,7 @@ data class ClientDataStatus(
|
|||
lastKeyRequestServerSequence?.let { writer.name(LAST_KEY_REQUEST_SEQUENCE).value(it) }
|
||||
lastKeyResponseServerSequence?.let { writer.name(LAST_KEY_RESPONSE_SEQUENCE).value(it) }
|
||||
dhKeyVersion?.let { writer.name(DH).value(it) }
|
||||
u2fVersion?.let { writer.name(U2F).value(it) }
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ data class ServerDataStatus(
|
|||
val pendingKeyRequests: List<ServerKeyRequest>,
|
||||
val keyResponses: List<ServerKeyResponse>,
|
||||
val dh: ServerDhKey?,
|
||||
val u2f: ServerU2fData?,
|
||||
val fullVersionUntil: Long,
|
||||
val message: String?,
|
||||
val apiLevel: Int
|
||||
|
@ -65,6 +66,7 @@ data class ServerDataStatus(
|
|||
private const val PENDING_KEY_REQUESTS = "krq"
|
||||
private const val KEY_RESPONSES = "kr"
|
||||
private const val DH = "dh"
|
||||
private const val U2F = "u2f"
|
||||
private const val FULL_VERSION_UNTIL = "fullVersion"
|
||||
private const val MESSAGE = "message"
|
||||
private const val API_LEVEL = "apiLevel"
|
||||
|
@ -83,6 +85,7 @@ data class ServerDataStatus(
|
|||
var pendingKeyRequests = emptyList<ServerKeyRequest>()
|
||||
var keyResponses = emptyList<ServerKeyResponse>()
|
||||
var dh: ServerDhKey? = null
|
||||
var u2f: ServerU2fData? = null
|
||||
var fullVersionUntil: Long? = null
|
||||
var message: String? = null
|
||||
var apiLevel = 0
|
||||
|
@ -103,6 +106,7 @@ data class ServerDataStatus(
|
|||
PENDING_KEY_REQUESTS -> pendingKeyRequests = ServerKeyRequest.parseList(reader)
|
||||
KEY_RESPONSES -> keyResponses = ServerKeyResponse.parseList(reader)
|
||||
DH -> dh = ServerDhKey.parse(reader)
|
||||
U2F -> u2f = ServerU2fData.parse(reader)
|
||||
FULL_VERSION_UNTIL -> fullVersionUntil = reader.nextLong()
|
||||
MESSAGE -> message = reader.nextString()
|
||||
API_LEVEL -> apiLevel = reader.nextInt()
|
||||
|
@ -125,6 +129,7 @@ data class ServerDataStatus(
|
|||
pendingKeyRequests = pendingKeyRequests,
|
||||
keyResponses = keyResponses,
|
||||
dh = dh,
|
||||
u2f = u2f,
|
||||
fullVersionUntil = fullVersionUntil!!,
|
||||
message = message,
|
||||
apiLevel = apiLevel
|
||||
|
@ -1297,3 +1302,75 @@ data class ServerDhKey(val version: String, val key: ByteArray) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerU2fData(
|
||||
val version: String,
|
||||
val data: List<ServerU2fItem>
|
||||
) {
|
||||
companion object {
|
||||
private const val VERSION = "v"
|
||||
private const val DATA = "d"
|
||||
|
||||
fun parse(reader: JsonReader): ServerU2fData {
|
||||
var version: String? = null
|
||||
var data: List<ServerU2fItem>? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
VERSION -> version = reader.nextString()
|
||||
DATA -> data = ServerU2fItem.parseList(reader)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerU2fData(
|
||||
version = version!!,
|
||||
data = data!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerU2fItem(
|
||||
val userId: String,
|
||||
val addedAt: Long,
|
||||
val keyHandle: ByteArray,
|
||||
val publicKey: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
private const val USER_ID = "u"
|
||||
private const val ADDED_AT = "a"
|
||||
private const val KEY_HANDLE = "h"
|
||||
private const val PUBLIC_KEY = "p"
|
||||
|
||||
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||
|
||||
fun parse(reader: JsonReader): ServerU2fItem {
|
||||
var userId: String? = null
|
||||
var addedAt: Long? = null
|
||||
var keyHandle: ByteArray? = null
|
||||
var publicKey: ByteArray? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
USER_ID -> userId = reader.nextString()
|
||||
ADDED_AT -> addedAt = reader.nextLong()
|
||||
KEY_HANDLE -> keyHandle = reader.nextString().parseBase64()
|
||||
PUBLIC_KEY -> publicKey = reader.nextString().parseBase64()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerU2fItem(
|
||||
userId = userId!!,
|
||||
addedAt = addedAt!!,
|
||||
keyHandle = keyHandle!!,
|
||||
publicKey = publicKey!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,6 +48,16 @@ object U2FResponse {
|
|||
val counter: UInt,
|
||||
val signature: ByteArray
|
||||
) {
|
||||
val raw by lazy {
|
||||
byteArrayOf(
|
||||
flags,
|
||||
counter.shr(24).toUByte().toByte(),
|
||||
counter.shr(16).toUByte().toByte(),
|
||||
counter.shr(8).toUByte().toByte(),
|
||||
counter.toUByte().toByte(),
|
||||
) + signature
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(rawResponse: U2fRawResponse): Login {
|
||||
if (rawResponse.payload.size < 5) throw U2FException.InvalidDataException()
|
||||
|
|
|
@ -22,6 +22,10 @@ import io.timelimit.android.R
|
|||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.crypto.DHHandshake
|
||||
import io.timelimit.android.extensions.toByteArray
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.sync.actions.ReportU2fLoginAction
|
||||
import io.timelimit.android.u2f.U2FApplicationId
|
||||
import io.timelimit.android.u2f.U2FSignatureValidation
|
||||
import io.timelimit.android.u2f.protocol.U2FDevice
|
||||
|
@ -31,7 +35,7 @@ import io.timelimit.android.u2f.util.U2FException
|
|||
import io.timelimit.android.u2f.util.U2FThread
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import io.timelimit.android.ui.main.AuthenticationMethod
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
object AuthTokenLoginProcessor {
|
||||
|
@ -45,18 +49,52 @@ object AuthTokenLoginProcessor {
|
|||
|
||||
runAsync {
|
||||
try {
|
||||
val hasFullVersion = model.logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
||||
|
||||
if (!hasFullVersion) {
|
||||
toast(R.string.update_primary_device_toast_requires_full_version)
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
device.connect().use { session ->
|
||||
val keys = Threads.database.executeAndWait { model.logic.database.u2f().getAllSync() }
|
||||
val random = SecureRandom()
|
||||
val (keys, serverDhKey) = Threads.database.executeAndWait {
|
||||
model.logic.database.runInTransaction {
|
||||
val u2f = model.logic.database.u2f().getAllSync()
|
||||
val dh = model.logic.database.config().getLastDhKeySync()
|
||||
|
||||
u2f to dh
|
||||
}
|
||||
}
|
||||
|
||||
val dhHandshake = serverDhKey?.let {
|
||||
Threads.crypto.executeAndWait {
|
||||
DHHandshake.fromServerKey(it)
|
||||
}
|
||||
}
|
||||
|
||||
val applicationId = U2FApplicationId.fromUrl(U2FApplicationId.URL)
|
||||
val challenge = if (dhHandshake == null) ByteArray(32).also { SecureRandom().nextBytes(it) }
|
||||
else {
|
||||
val binaryDhKey = dhHandshake.keyVersion.toByteArray(Charsets.UTF_8)
|
||||
|
||||
MessageDigest.getInstance("SHA256").also {
|
||||
it.update(binaryDhKey.size.toByteArray())
|
||||
it.update(binaryDhKey)
|
||||
|
||||
it.update(dhHandshake.otherPublicKey.size.toByteArray())
|
||||
it.update(dhHandshake.otherPublicKey)
|
||||
|
||||
it.update(dhHandshake.ownPublicKey.size.toByteArray())
|
||||
it.update(dhHandshake.ownPublicKey)
|
||||
}.digest()
|
||||
}
|
||||
|
||||
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(
|
||||
|
@ -121,13 +159,28 @@ object AuthTokenLoginProcessor {
|
|||
return@runAsync
|
||||
}
|
||||
|
||||
model.setAuthenticatedUser(
|
||||
AuthenticatedUser(
|
||||
val authenticatedUser = if (dhHandshake == null) {
|
||||
AuthenticatedUser.LocalAuth.U2f(userId = key.userId)
|
||||
} else {
|
||||
val serverKeyId = Threads.crypto.executeAndWait {
|
||||
key.calculateServerKeyIdSync()
|
||||
}
|
||||
|
||||
AuthenticatedUser.U2fSigned(
|
||||
userId = key.userId,
|
||||
authenticatedBy = AuthenticationMethod.KeyCode,
|
||||
firstPasswordHash = userEntry.password,
|
||||
secondPasswordHash = "u2f"
|
||||
u2fServerKeyId = serverKeyId,
|
||||
u2fClientKeyId = key.keyId,
|
||||
signature = response,
|
||||
dh = dhHandshake
|
||||
)
|
||||
}
|
||||
|
||||
model.setAuthenticatedUser(authenticatedUser)
|
||||
|
||||
ActivityViewModel.dispatchWithoutCheckOrCatching(
|
||||
action = ReportU2fLoginAction,
|
||||
authenticatedUser = authenticatedUser,
|
||||
logic = model.logic
|
||||
)
|
||||
|
||||
return@runAsync // no need to try more
|
||||
|
|
|
@ -39,7 +39,6 @@ import io.timelimit.android.sync.actions.apply.ApplyActionChildAuthentication
|
|||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import io.timelimit.android.ui.main.AuthenticationMethod
|
||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
@ -210,11 +209,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
|
||||
if (shouldSignIn) {
|
||||
model.setAuthenticatedUser(AuthenticatedUser(
|
||||
model.setAuthenticatedUser(AuthenticatedUser.Password(
|
||||
userId = user.id,
|
||||
firstPasswordHash = user.password,
|
||||
secondPasswordHash = Threads.crypto.executeAndWait { PasswordHashing.hashSyncWithSalt("", user.secondPasswordSalt) },
|
||||
authenticatedBy = AuthenticationMethod.Password
|
||||
secondPasswordHash = Threads.crypto.executeAndWait { PasswordHashing.hashSyncWithSalt("", user.secondPasswordSalt) }
|
||||
))
|
||||
|
||||
isLoginDone.value = true
|
||||
|
@ -278,11 +276,8 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
|
||||
if (shouldSignIn) {
|
||||
// this feature is limited to the local mode
|
||||
model.setAuthenticatedUser(AuthenticatedUser(
|
||||
userId = user.id,
|
||||
firstPasswordHash = user.password,
|
||||
secondPasswordHash = "device",
|
||||
authenticatedBy = AuthenticationMethod.Password
|
||||
model.setAuthenticatedUser(AuthenticatedUser.LocalAuth.ScanCode(
|
||||
userId = user.id
|
||||
))
|
||||
|
||||
isLoginDone.value = true
|
||||
|
@ -326,11 +321,10 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
|
||||
val secondPasswordHash = Threads.crypto.executeAndWait { PasswordHashing.hashSyncWithSalt(password, userEntry.secondPasswordSalt) }
|
||||
|
||||
val authenticatedUser = AuthenticatedUser(
|
||||
val authenticatedUser = AuthenticatedUser.Password(
|
||||
userId = userEntry.id,
|
||||
firstPasswordHash = userEntry.password,
|
||||
secondPasswordHash = secondPasswordHash,
|
||||
authenticatedBy = AuthenticationMethod.Password
|
||||
secondPasswordHash = secondPasswordHash
|
||||
)
|
||||
|
||||
val allowLoginStatus = Threads.database.executeAndWait {
|
||||
|
|
|
@ -21,19 +21,19 @@ import android.widget.Toast
|
|||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.map
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.crypto.DHHandshake
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.ParentAction
|
||||
import io.timelimit.android.sync.actions.apply.*
|
||||
import io.timelimit.android.u2f.protocol.U2FResponse
|
||||
|
||||
class ActivityViewModel(application: Application): AndroidViewModel(application) {
|
||||
companion object {
|
||||
|
@ -47,10 +47,7 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
|||
ApplyActionUtil.applyParentAction(
|
||||
action = action,
|
||||
database = logic.database,
|
||||
authentication = ApplyActionParentPasswordAuthentication(
|
||||
parentUserId = authenticatedUser.userId,
|
||||
secondPasswordHash = authenticatedUser.secondPasswordHash
|
||||
),
|
||||
authentication = ApplyActionUserAuthentication(authenticatedUser),
|
||||
syncUtil = logic.syncUtil,
|
||||
platformIntegration = logic.platformIntegration
|
||||
)
|
||||
|
@ -79,41 +76,45 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
|||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
private val authenticatedChild: LiveData<Pair<ApplyActionParentAuthentication, User>?> = deviceUser.map { user ->
|
||||
private val authenticatedUserChecked = authenticatedUserMetadata.switchMap { user ->
|
||||
val userEntryLive =
|
||||
if (user == null) liveDataFromNullableValue(null)
|
||||
else database.user().getUserByIdLive(user.userId)
|
||||
|
||||
userEntryLive.switchMap { userEntry ->
|
||||
if (userEntry == null) liveDataFromNullableValue(null)
|
||||
else {
|
||||
val stillValid = when (user) {
|
||||
is AuthenticatedUser.Password -> liveDataFromNonNullValue( user.firstPasswordHash == userEntry.password)
|
||||
is AuthenticatedUser.U2fSigned -> logic.database.u2f().getByClientKeyIdLive(user.u2fClientKeyId).map { keyEntry ->
|
||||
keyEntry != null && keyEntry.userId == user.userId
|
||||
}
|
||||
is AuthenticatedUser.LocalAuth -> logic.fullVersion.isLocalMode
|
||||
null -> liveDataFromNonNullValue(false)
|
||||
}
|
||||
|
||||
stillValid.map { valid ->
|
||||
if (valid && user != null) ApplyActionUserAuthentication(user) to userEntry
|
||||
else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val authenticatedChildChecked: LiveData<Pair<ApplyActionParentAuthentication, User>?> = deviceUser.map { user ->
|
||||
if (user?.type == UserType.Child && user.allowSelfLimitAdding) {
|
||||
ApplyActionChildAddLimitAuthentication as ApplyActionParentAuthentication to user
|
||||
} else null
|
||||
}
|
||||
|
||||
val authenticatedUserOrChild: LiveData<Pair<ApplyActionParentAuthentication, User>?> = userWhichIsKeptSignedIn.switchMap { signedInUser ->
|
||||
if (signedInUser != null) {
|
||||
liveDataFromNullableValue(
|
||||
(ApplyActionParentDeviceAuthentication to signedInUser)
|
||||
as Pair<ApplyActionParentAuthentication, User>?
|
||||
)
|
||||
} else {
|
||||
authenticatedUserMetadata.switchMap {
|
||||
authenticatedUser ->
|
||||
|
||||
if (authenticatedUser == null) {
|
||||
authenticatedChild
|
||||
} else {
|
||||
database.user().getUserByIdLive(authenticatedUser.userId).switchMap {
|
||||
if (it == null || it.password != authenticatedUser.firstPasswordHash) {
|
||||
authenticatedChild
|
||||
} else {
|
||||
liveDataFromNullableValue(
|
||||
(ApplyActionParentPasswordAuthentication(
|
||||
parentUserId = authenticatedUser.userId,
|
||||
secondPasswordHash = authenticatedUser.secondPasswordHash
|
||||
) to it) as Pair<ApplyActionParentAuthentication, User>?
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val authenticatedUserOrChild: LiveData<Pair<ApplyActionParentAuthentication, User>?> =
|
||||
mergeLiveDataWaitForValues(userWhichIsKeptSignedIn, authenticatedChildChecked, authenticatedUserChecked)
|
||||
.map { (keptSignedIn, localChild, authenticatedUser) ->
|
||||
keptSignedIn?.let {
|
||||
ApplyActionParentDeviceAuthentication to it
|
||||
} ?: authenticatedUser ?: localChild
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoreUnchanged()
|
||||
|
||||
val authenticatedUser = authenticatedUserOrChild.map { if (it?.second?.type != UserType.Parent) null else it }
|
||||
|
||||
|
@ -198,13 +199,25 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
|
||||
data class AuthenticatedUser (
|
||||
val userId: String,
|
||||
val firstPasswordHash: String,
|
||||
val secondPasswordHash: String,
|
||||
val authenticatedBy: AuthenticationMethod
|
||||
)
|
||||
sealed class AuthenticatedUser {
|
||||
abstract val userId: String
|
||||
|
||||
enum class AuthenticationMethod {
|
||||
Password, KeyCode
|
||||
data class Password(
|
||||
override val userId: String,
|
||||
val firstPasswordHash: String,
|
||||
val secondPasswordHash: String
|
||||
): AuthenticatedUser()
|
||||
|
||||
data class U2fSigned(
|
||||
override val userId: String,
|
||||
val u2fServerKeyId: String,
|
||||
val u2fClientKeyId: Long,
|
||||
val signature: U2FResponse.Login,
|
||||
val dh: DHHandshake
|
||||
): AuthenticatedUser()
|
||||
|
||||
sealed class LocalAuth: AuthenticatedUser() {
|
||||
data class U2f(override val userId: String): LocalAuth()
|
||||
data class ScanCode(override val userId: String): LocalAuth()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -23,9 +23,7 @@ import io.timelimit.android.livedata.castDown
|
|||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.livedata.waitUntilValueMatches
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionChildAddLimitAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentDeviceAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyDirectCallAuthentication
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -65,21 +63,13 @@ class AddDeviceModel(application: Application): AndroidViewModel(application) {
|
|||
statusInternal.value = Failed
|
||||
} else {
|
||||
try {
|
||||
val auth = user.first
|
||||
val auth = ApplyDirectCallAuthentication.from(user.first)
|
||||
|
||||
val response = when (auth) {
|
||||
ApplyActionParentDeviceAuthentication -> server.api.createAddDeviceToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = "",
|
||||
parentPasswordSecondHash = "device"
|
||||
)
|
||||
is ApplyActionParentPasswordAuthentication -> server.api.createAddDeviceToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = auth.parentUserId,
|
||||
parentPasswordSecondHash = auth.secondPasswordHash
|
||||
)
|
||||
is ApplyActionChildAddLimitAuthentication -> throw RuntimeException("child can not do that")
|
||||
}
|
||||
val response = server.api.createAddDeviceToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = auth.parentUserId,
|
||||
parentPasswordSecondHash = auth.parentPasswordSecondHash
|
||||
)
|
||||
|
||||
statusInternal.value = ShowingToken(response.token)
|
||||
|
||||
|
|
|
@ -25,9 +25,7 @@ import io.timelimit.android.R
|
|||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionChildAddLimitAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentDeviceAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyDirectCallAuthentication
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
|
||||
class RemoveDeviceModel(application: Application): AndroidViewModel(application) {
|
||||
|
@ -54,28 +52,18 @@ class RemoveDeviceModel(application: Application): AndroidViewModel(application)
|
|||
if (!server.hasAuthToken) {
|
||||
Toast.makeText(getApplication(), R.string.remove_device_local_mode, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
val parent = activityViewModel.authenticatedUser.value?.first
|
||||
val auth = activityViewModel.authenticatedUser.value?.first?.let {
|
||||
ApplyDirectCallAuthentication.from(it)
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
if (auth != null) {
|
||||
try {
|
||||
when (parent) {
|
||||
ApplyActionParentDeviceAuthentication -> server.api.removeDevice(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = "",
|
||||
parentPasswordSecondHash = "device",
|
||||
deviceId = deviceId
|
||||
)
|
||||
is ApplyActionParentPasswordAuthentication -> server.api.removeDevice(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = parent.parentUserId,
|
||||
parentPasswordSecondHash = parent.secondPasswordHash,
|
||||
deviceId = deviceId
|
||||
)
|
||||
is ApplyActionChildAddLimitAuthentication -> {
|
||||
// caught below
|
||||
throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
server.api.removeDevice(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = auth.parentUserId,
|
||||
parentPasswordSecondHash = auth.parentPasswordSecondHash,
|
||||
deviceId = deviceId
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "removing device failed", ex)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -95,6 +95,11 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
binding.isUsingLocalMode = isLocalMode
|
||||
})
|
||||
|
||||
logic.serverApiLevelLogic.infoLive
|
||||
.map { it.hasLevelOrIsOffline(6) }
|
||||
.observe(viewLifecycleOwner)
|
||||
{ binding.hasU2fSupport = it }
|
||||
|
||||
DeleteParentView.bind(
|
||||
view = binding.deleteParent,
|
||||
lifecycleOwner = this,
|
||||
|
|
|
@ -29,9 +29,10 @@ import io.timelimit.android.livedata.castDown
|
|||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.ChangeParentPasswordAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUserAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.network.ParentPassword
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import java.nio.charset.Charset
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
|
@ -155,14 +156,17 @@ class ChangeParentPasswordViewModel(application: Application): AndroidViewModel(
|
|||
}
|
||||
|
||||
ApplyActionUtil.applyParentAction(
|
||||
action,
|
||||
logic.database,
|
||||
ApplyActionParentPasswordAuthentication(
|
||||
parentUserId = parentUserId,
|
||||
secondPasswordHash = oldPasswordSecondHash
|
||||
),
|
||||
logic.syncUtil,
|
||||
logic.platformIntegration
|
||||
action,
|
||||
logic.database,
|
||||
ApplyActionUserAuthentication(
|
||||
AuthenticatedUser.Password(
|
||||
userId = parentUserId,
|
||||
firstPasswordHash = userEntry.password,
|
||||
secondPasswordHash = oldPasswordSecondHash
|
||||
)
|
||||
),
|
||||
logic.syncUtil,
|
||||
logic.platformIntegration
|
||||
)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
|
|
|
@ -77,14 +77,17 @@ class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
override fun onRemoveClicked(keyItem: U2FKeyListItem.KeyItem) {
|
||||
if (isAuthValidOrShowMessage()) {
|
||||
if (activityModel.getAuthenticatedUser()?.authenticatedBy == AuthenticationMethod.Password) {
|
||||
if (
|
||||
activityModel.getAuthenticatedUser() is AuthenticatedUser.U2fSigned ||
|
||||
activityModel.getAuthenticatedUser() is AuthenticatedUser.LocalAuth.U2f
|
||||
) {
|
||||
U2FRequiresPasswordForRemovalDialogFragment.newInstance().show(parentFragmentManager)
|
||||
} else {
|
||||
RemoveU2FKeyDialogFragment.newInstance(
|
||||
userId = params.userId,
|
||||
keyHandle = keyItem.item.keyHandle,
|
||||
publicKey = keyItem.item.publicKey
|
||||
).show(parentFragmentManager)
|
||||
} else {
|
||||
U2FRequiresPasswordForRemovalDialogFragment.newInstance().show(parentFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,7 @@ import io.timelimit.android.extensions.BillingNotSupportedException
|
|||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionChildAddLimitAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentDeviceAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyDirectCallAuthentication
|
||||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||
import io.timelimit.android.sync.network.api.NotFoundHttpError
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
|
@ -78,25 +76,16 @@ class PurchaseModel(application: Application): AndroidViewModel(application) {
|
|||
|
||||
if (!BuildConfig.storeCompilant) {
|
||||
if (auth.isParentAuthenticated()) {
|
||||
val authData = auth.authenticatedUser.value?.first
|
||||
|
||||
try {
|
||||
val token = when (authData) {
|
||||
ApplyActionParentDeviceAuthentication -> server.api.createIdentityToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = "",
|
||||
parentPasswordSecondHash = "device"
|
||||
)
|
||||
is ApplyActionParentPasswordAuthentication -> server.api.createIdentityToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = authData.parentUserId,
|
||||
parentPasswordSecondHash = authData.secondPasswordHash
|
||||
)
|
||||
is ApplyActionChildAddLimitAuthentication -> throw RuntimeException(
|
||||
"child can not do that"
|
||||
)
|
||||
null -> throw RuntimeException("missing user")
|
||||
}
|
||||
val authData = ApplyDirectCallAuthentication.from(
|
||||
auth.authenticatedUser.value?.first!!
|
||||
)
|
||||
|
||||
val token = server.api.createIdentityToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = authData.parentUserId,
|
||||
parentPasswordSecondHash = authData.parentPasswordSecondHash
|
||||
)
|
||||
|
||||
statusInternal.value = Status.ReadyToken(token)
|
||||
} catch (ex: NotFoundHttpError) {
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
name="isUsingLocalMode"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="hasU2fSupport"
|
||||
type="boolean" />
|
||||
|
||||
<variable
|
||||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.parent.ManageParentFragmentHandlers" />
|
||||
|
@ -158,7 +162,7 @@
|
|||
layout="@layout/manage_parent_notifications" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:visibility="@{isUsingLocalMode ? View.VISIBLE : View.GONE}"
|
||||
android:visibility="@{hasU2fSupport ? View.VISIBLE : View.GONE}"
|
||||
android:onClick="@{() -> handlers.onManageU2FClicked()}"
|
||||
android:foreground="?selectableItemBackground"
|
||||
app:cardUseCompatPadding="true"
|
||||
|
@ -182,6 +186,12 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/purchase_required_info_local_mode_free"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
|
|
@ -1688,7 +1688,7 @@
|
|||
<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>
|
||||
<string name="manage_parent_u2f_remove_key_requires_password">Schlüssel können nur nach einer Anmeldung mittels Passwort entfernt werden</string>
|
||||
<string name="manage_parent_u2f_remove_key_requires_password">Schlüssel können nur nach einer Anmeldung ohne Schlüssel entfernt werden</string>
|
||||
|
||||
<string name="manage_parent_u2f_err_wrong_user">Nur der Benutzer selbst kann seine U2F-Schlüssel verwalten</string>
|
||||
|
||||
|
|
|
@ -1735,7 +1735,7 @@
|
|||
<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>
|
||||
<string name="manage_parent_u2f_remove_key_requires_password">You can only remove keys after signing in by password</string>
|
||||
<string name="manage_parent_u2f_remove_key_requires_password">You can only remove keys after signing in without a key</string>
|
||||
|
||||
<string name="manage_parent_u2f_err_wrong_user">Only the user itself can manage its U2F Keys</string>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue