yubioath-flutter/qml/Main.qml

492 lines
14 KiB
QML
Raw Normal View History

2017-03-07 13:58:08 +03:00
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.1
2017-02-02 16:16:52 +03:00
import QtQuick.Controls.Styles 1.4
import QtQuick.Dialogs 1.2
import Qt.labs.settings 1.0
ApplicationWindow {
2017-02-06 17:00:11 +03:00
id: appWindow
2017-02-02 16:16:52 +03:00
width: 300
height: 400
2017-02-04 00:28:57 +03:00
minimumHeight: 400
minimumWidth: 300
visible: true
2017-03-16 17:10:15 +03:00
title: getTitle()
2017-02-07 12:12:22 +03:00
property var device: yk
property var credentials: device.credentials
2017-02-07 16:17:54 +03:00
property bool hasDevice: device.hasDevice
2017-03-17 15:15:48 +03:00
property bool canShowCredentials: device.hasDevice && modeAndKeyMatch
&& device.validated
2017-03-16 17:29:50 +03:00
property bool modeAndKeyMatch: slotModeMatch || ccidModeMatch
property bool slotModeMatch: (settings.slotMode && device.hasOTP)
property bool ccidModeMatch: (!settings.slotMode && device.hasCCID)
2017-02-17 16:09:55 +03:00
property var hotpCoolDowns: []
2017-03-01 15:17:47 +03:00
// Don't refresh credentials when window is minimized or hidden
// See http://doc.qt.io/qt-5/qwindow.html#Visibility-enum
property bool shouldRefresh: visibility != 3 && visibility != 0
2017-03-17 11:07:41 +03:00
onHasDeviceChanged: handleNewDevice()
2017-03-17 11:36:50 +03:00
onCredentialsChanged: {
hotpTouchTimer.stop()
touchYourYubikey.close()
}
2017-03-16 17:41:26 +03:00
menuBar: MainMenuBar {
slotMode: settings.slotMode
hasDevice: device.hasDevice
onOpenAddCredential: openClearAddCredential()
onOpenSetPassword: setPassword.open()
onOpenReset: reset.open()
onOpenSettings: settingsDialog.open()
onOpenAbout: aboutPage.open()
}
2017-02-28 14:08:42 +03:00
SystemPalette {
id: palette
}
2017-03-16 17:41:26 +03:00
// This information is stored in the system registry on Windows,
// and in XML preferences files on macOS. On other Unix systems,
// in the absence of a standard, INI text files are used.
// See http://doc.qt.io/qt-5/qml-qt-labs-settings-settings.html#details
Settings {
id: settings
property bool slotMode
property bool slot1
property bool slot2
property var slot1digits
property var slot2digits
2017-03-01 13:57:40 +03:00
property string savedPasswords
}
AboutPage {
id: aboutPage
}
2017-02-03 11:58:17 +03:00
AddCredential {
id: addCredential
device: yk
}
AddCredentialSlot {
id: addCredentialSlot
2017-03-01 16:38:02 +03:00
settings: settings
device: yk
2017-02-03 11:58:17 +03:00
}
SettingsDialog {
id: settingsDialog
settings: settings
onAccepted: {
2017-03-16 17:40:51 +03:00
saveSettings()
2017-02-23 14:18:20 +03:00
refreshDependingOnMode(true)
}
2017-02-20 14:27:10 +03:00
}
2017-02-10 14:44:10 +03:00
SetPassword {
id: setPassword
2017-02-16 17:22:20 +03:00
onAccepted: {
2017-03-16 18:34:00 +03:00
trySetPassword()
passwordUpdated.open()
2017-03-18 20:27:40 +03:00
setPassword.clear()
2017-02-16 17:22:20 +03:00
}
}
2017-03-17 10:48:38 +03:00
PasswordSetConfirmation {
2017-02-10 16:01:49 +03:00
id: passwordUpdated
}
2017-03-17 10:57:30 +03:00
Reset {
2017-02-17 10:52:34 +03:00
id: reset
onAccepted: {
device.reset()
2017-03-08 10:56:40 +03:00
device.refreshCCIDCredentials(true)
2017-02-17 10:52:34 +03:00
}
}
2017-02-17 12:50:17 +03:00
PasswordPrompt {
id: passwordPrompt
2017-03-17 13:01:59 +03:00
onAccepted: handlePasswordEntered()
2017-03-01 13:57:40 +03:00
}
2017-02-17 12:50:17 +03:00
// @disable-check M301
YubiKey {
id: yk
2017-03-17 11:36:50 +03:00
onError: console.log(error)
onWrongPassword: passwordPrompt.open()
2017-02-17 12:50:17 +03:00
}
2017-03-17 11:36:50 +03:00
NoLoadedDeviceMessage {
2017-02-17 12:50:17 +03:00
id: noLoadedDeviceMessage
2017-03-17 11:36:50 +03:00
device: yk
2017-02-17 12:50:17 +03:00
}
2017-03-17 12:19:06 +03:00
LoadedDeviceMessage {
id: loadedDeviceMessage
device: yk
nCredentials: filteredCredentials(credentials).length
readingCredentials: credentials === null
settings: settings
2017-02-28 11:32:48 +03:00
}
2017-03-17 12:24:32 +03:00
ClipBoard {
2017-02-17 12:50:17 +03:00
id: clipboard
2017-02-03 18:23:28 +03:00
}
2017-02-03 11:58:17 +03:00
2017-03-17 13:01:59 +03:00
CredentialMenu {
id: credentialMenu
2017-03-17 13:01:59 +03:00
credential: repeater.selected
showGenerate: allowManualGenerate(repeater.selected)
enableGenerate: enableManualGenerate(repeater.selected)
onGenerate: handleGenerate(repeater.selected)
onDeleteCredential: confirmDeleteCredential.open()
onCopy: clipboard.setClipboard(repeater.selected.code)
2017-02-03 18:23:28 +03:00
}
2017-02-03 11:58:17 +03:00
DeleteCredentialConfirmation {
2017-02-17 12:50:17 +03:00
id: confirmDeleteCredential
onAccepted: {
deleteSelectedCredential()
2017-02-23 18:00:17 +03:00
refreshDependingOnMode(true)
2017-02-17 12:50:17 +03:00
}
}
TouchYubiKey {
2017-02-17 12:50:17 +03:00
id: touchYourYubikey
2017-02-17 12:14:37 +03:00
}
2017-03-17 14:21:31 +03:00
ArrowKeysSelecter {
2017-02-16 13:07:41 +03:00
id: arrowKeys
2017-03-17 14:21:31 +03:00
credRepeater: repeater
2017-02-16 13:07:41 +03:00
}
2017-02-06 17:00:11 +03:00
ColumnLayout {
2017-02-06 15:42:17 +03:00
anchors.fill: parent
2017-02-06 17:00:11 +03:00
2017-03-17 14:45:43 +03:00
TimeLeftBar {
id: timeLeftBar
shouldBeVisible: canShowCredentials
2017-02-06 17:00:11 +03:00
}
2017-02-02 16:16:52 +03:00
2017-02-06 17:00:11 +03:00
ScrollView {
id: scrollView
Layout.fillHeight: true
Layout.fillWidth: true
ColumnLayout {
width: scrollView.viewport.width
id: credentialsColumn
visible: device.hasDevice && (ccidModeMatch || slotModeMatch)
2017-02-06 17:07:43 +03:00
anchors.right: appWindow.right
anchors.left: appWindow.left
anchors.top: appWindow.top
2017-02-06 17:00:11 +03:00
Repeater {
2017-02-06 17:57:32 +03:00
id: repeater
2017-02-28 11:32:48 +03:00
model: filteredCredentials(credentials)
2017-03-17 14:52:34 +03:00
property var selected: null
2017-03-17 14:21:31 +03:00
property var selectedIndex: null
2017-02-06 17:00:11 +03:00
Rectangle {
id: credentialRectangle
2017-02-16 13:07:41 +03:00
focus: true
2017-03-17 15:15:48 +03:00
color: getCredentialColor(index, repeater.selected,
modelData)
2017-02-06 17:00:11 +03:00
Layout.fillWidth: true
Layout.minimumHeight: 70
Layout.alignment: Qt.AlignTop
MouseArea {
anchors.fill: parent
2017-03-17 15:15:48 +03:00
onClicked: handleMouseClick(mouse, index,
repeater.selected,
repeater.selectedIndex,
modelData)
acceptedButtons: Qt.RightButton | Qt.LeftButton
2017-02-06 15:42:17 +03:00
}
2017-02-06 17:00:11 +03:00
ColumnLayout {
anchors.leftMargin: 10
2017-03-07 13:58:08 +03:00
anchors.topMargin: 5
anchors.bottomMargin: 5
2017-02-06 17:00:11 +03:00
anchors.fill: parent
Text {
visible: hasIssuer(modelData.name)
2017-03-17 15:52:13 +03:00
text: qsTr("") + parseIssuer(modelData.name)
2017-03-09 12:26:11 +03:00
font.pixelSize: 12
2017-02-06 17:00:11 +03:00
}
2017-02-15 17:14:34 +03:00
Text {
opacity: isExpired(modelData) ? 0.6 : 1
2017-03-17 15:52:13 +03:00
visible: modelData.code !== null
text: qsTr("") + modelData.code
2017-02-06 17:00:11 +03:00
font.family: "Verdana"
2017-03-09 12:26:11 +03:00
font.pixelSize: 20
2017-02-06 17:00:11 +03:00
}
Text {
text: hasIssuer(
modelData.name) ? qsTr(
2017-03-17 15:52:13 +03:00
"") + parseName(
2017-02-06 17:00:11 +03:00
modelData.name) : modelData.name
2017-03-09 12:26:11 +03:00
font.pixelSize: 12
2017-02-06 17:00:11 +03:00
}
2017-02-06 15:42:17 +03:00
}
2017-02-02 16:16:52 +03:00
}
}
}
}
2017-02-06 17:00:11 +03:00
2017-02-06 17:07:43 +03:00
TextField {
id: search
visible: canShowCredentials && device.hasAnyCredentials()
2017-03-16 16:04:50 +03:00
placeholderText: qsTr("Search...")
2017-02-06 17:07:43 +03:00
Layout.fillWidth: true
2017-03-15 16:57:01 +03:00
KeyNavigation.tab: scrollView
2017-02-06 17:00:11 +03:00
}
2017-02-02 16:16:52 +03:00
}
2017-02-06 17:57:32 +03:00
Timer {
2017-02-02 16:16:52 +03:00
id: ykTimer
triggeredOnStart: true
interval: 500
repeat: true
running: true
2017-02-23 12:40:24 +03:00
onTriggered: device.refresh(refreshDependingOnMode)
}
2017-02-02 16:16:52 +03:00
Timer {
2017-03-17 15:52:13 +03:00
id: timeLeftTimer
2017-02-02 16:16:52 +03:00
interval: 100
repeat: true
running: true
triggeredOnStart: true
2017-03-17 15:52:13 +03:00
onTriggered: checkTimeLeft()
}
2017-02-17 12:50:17 +03:00
Timer {
2017-02-17 16:09:55 +03:00
id: hotpCoolDownTimer
2017-02-17 12:50:17 +03:00
interval: 5000
2017-02-17 16:09:55 +03:00
onTriggered: hotpCoolDowns = []
}
Timer {
id: hotpTouchTimer
interval: 500
onTriggered: touchYourYubikey.open()
}
2017-03-17 15:52:13 +03:00
function checkTimeLeft() {
var timeLeft = device.expiration - (Date.now() / 1000)
if (timeLeft <= 0 && timeLeftBar.value > 0) {
refreshDependingOnMode(true)
}
timeLeftBar.value = timeLeft
}
2017-03-09 12:49:42 +03:00
function allowManualGenerate(cred) {
2017-03-17 15:52:13 +03:00
return cred !== null && (cred.oath_type === "hotp"
|| repeater.selected.touch)
2017-03-09 12:49:42 +03:00
}
function enableManualGenerate(cred) {
2017-03-17 13:01:59 +03:00
if (allowManualGenerate(cred)) {
2017-03-17 15:52:13 +03:00
if (cred.oath_type !== "hotp") {
return cred.code === null || isExpired(repeater.selected)
2017-03-17 13:01:59 +03:00
} else {
return !isInCoolDown(cred.name)
}
2017-03-09 12:49:42 +03:00
} else {
2017-03-17 13:01:59 +03:00
return false
2017-03-09 12:49:42 +03:00
}
}
function isExpired(cred) {
2017-03-17 15:52:13 +03:00
return cred !== null && (cred.oath_type !== "hotp")
&& (cred.expiration - (Date.now() / 1000) <= 0)
2017-03-09 12:49:42 +03:00
}
2017-03-09 12:26:11 +03:00
2017-03-09 12:49:42 +03:00
function rememberPassword() {
var deviceId = device.oathId
settings.savedPasswords += deviceId + ':' + device.passwordKey + ';'
}
2017-02-23 14:18:20 +03:00
function refreshDependingOnMode(force) {
2017-03-01 15:17:47 +03:00
if (hasDevice && shouldRefresh) {
2017-02-28 12:57:10 +03:00
if (settings.slotMode && device.hasOTP) {
2017-02-28 14:08:42 +03:00
device.refreshSlotCredentials([settings.slot1, settings.slot2],
getSlotDigitsSettings(), force)
2017-02-28 12:57:10 +03:00
} else if (!settings.slotMode && device.hasCCID) {
2017-02-23 14:18:20 +03:00
device.refreshCCIDCredentials(force)
}
}
}
function getSlotDigitsSettings() {
var slot1digits = settings.slot1digits === 1 ? 8 : 6
var slot2digits = settings.slot2digits === 1 ? 8 : 6
return [slot1digits, slot2digits]
}
2017-02-28 11:32:48 +03:00
function filteredCredentials(creds) {
2017-02-06 17:57:32 +03:00
var result = []
2017-03-17 15:52:13 +03:00
if (creds !== null) {
2017-02-06 17:57:32 +03:00
for (var i = 0; i < creds.length; i++) {
var cred = creds[i]
2017-02-28 11:32:48 +03:00
if (cred.name.toLowerCase().indexOf(search.text.toLowerCase(
2017-02-06 17:57:32 +03:00
)) !== -1) {
result.push(creds[i])
}
}
}
return result
}
2017-02-17 12:14:37 +03:00
function isInCoolDown(name) {
2017-03-08 16:50:50 +03:00
return hotpCoolDowns.indexOf(name) !== -1
2017-02-17 12:14:37 +03:00
}
2017-03-09 12:49:42 +03:00
2017-02-02 17:44:49 +03:00
function hasIssuer(name) {
return name.indexOf(':') !== -1
}
2017-03-09 12:49:42 +03:00
2017-02-02 17:44:49 +03:00
function parseName(name) {
return name.split(":").slice(1).join(":")
}
2017-03-09 12:49:42 +03:00
2017-02-02 17:44:49 +03:00
function parseIssuer(name) {
return name.split(":", 1)
}
2017-02-02 16:16:52 +03:00
function calculateCredential(credential) {
if (settings.slotMode) {
2017-02-23 18:00:17 +03:00
var slot = getSlot(credential.name)
var digits = getDigits(slot)
device.calculateSlotMode(slot, digits)
} else {
device.calculate(credential)
}
2017-03-17 15:52:13 +03:00
if (credential.oath_type === "hotp") {
2017-02-17 16:09:55 +03:00
hotpTouchTimer.restart()
}
2017-02-02 16:16:52 +03:00
if (credential.touch) {
touchYourYubikey.open()
}
}
2017-02-23 18:00:17 +03:00
function getSlot(name) {
if (name.indexOf('1') !== -1) {
return 1
}
2017-02-23 18:00:17 +03:00
if (name.indexOf('2') !== -1) {
return 2
}
}
function getDigits(slot) {
2017-02-28 14:08:42 +03:00
return getSlotDigitsSettings()[slot - 1]
}
function openClearAddCredential() {
if (settings.slotMode) {
addCredentialSlot.clear()
device.getSlotStatus(addCredentialSlot.open)
} else {
addCredential.clear()
addCredential.open()
}
}
2017-03-16 17:10:15 +03:00
function getTitle() {
return qsTr("Yubico Authenticator") + (settings.slotMode ? qsTr(" [Slot mode]") : '')
}
2017-03-16 18:34:00 +03:00
function saveSettings() {
settings.slotMode = settingsDialog.slotMode
settings.slot1 = settingsDialog.slot1
settings.slot2 = settingsDialog.slot2
settings.slot1digits = settingsDialog.slot1digits
settings.slot2digits = settingsDialog.slot2digits
}
function trySetPassword() {
if (setPassword.newPassword.length > 0) {
device.setPassword(setPassword.newPassword)
} else {
device.setPassword(null)
}
}
2017-03-17 11:07:41 +03:00
function handleNewDevice() {
if (device.hasDevice && ccidModeMatch) {
device.promptOrSkip(passwordPrompt, settings.savedPasswords)
} else {
passwordPrompt.close()
setPassword.close()
addCredential.close()
addCredentialSlot.close()
}
}
2017-03-17 13:01:59 +03:00
function handleGenerate(cred) {
if (!isInCoolDown(cred.name)) {
calculateCredential(cred)
if (cred.oath_type === "hotp") {
hotpCoolDowns.push(cred.name)
hotpCoolDownTimer.restart()
}
}
}
function handlePasswordEntered() {
if (passwordPrompt.remember) {
device.validate(passwordPrompt.password, rememberPassword)
} else {
device.validate(passwordPrompt.password)
}
passwordPrompt.clear()
}
function deleteSelectedCredential() {
if (settings.slotMode) {
device.deleteSlotCredential(getSlot(repeater.selected.name))
} else {
device.deleteCredential(repeater.selected)
}
}
2017-03-17 14:52:34 +03:00
function getCredentialColor(index, selected, modelData) {
if (selected !== null && selected.name === modelData.name) {
return palette.dark
}
if (index % 2 == 0) {
return "#00000000"
}
return palette.alternateBase
}
2017-03-17 15:15:48 +03:00
function handleMouseClick(mouse, index, selected, selectedIndex, modelData) {
arrowKeys.forceActiveFocus()
if (mouse.button & Qt.LeftButton) {
if (selected !== null && selected.name === modelData.name) {
// Unselect
repeater.selected = null
repeater.selectedIndex = null
} else {
// Select
repeater.selected = modelData
repeater.selectedIndex = index
}
}
if (mouse.button & Qt.RightButton) {
repeater.selected = modelData
repeater.selectedIndex = index
credentialMenu.popup()
}
}
}