yubioath-flutter/qml/Main.qml

660 lines
18 KiB
QML
Raw Normal View History

2017-02-02 16:16:52 +03:00
import QtQuick 2.6
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-01-27 16:28:06 +03:00
title: qsTr("Yubico Authenticator")
2017-02-07 12:12:22 +03:00
property var device: yk
2017-02-02 16:16:52 +03:00
property int expiration: 0
2017-02-07 12:12:22 +03:00
property var credentials: device.credentials
2017-02-07 16:17:54 +03:00
property bool validated: device.validated
property bool hasDevice: device.hasDevice
2017-02-17 16:09:55 +03:00
property var hotpCoolDowns: []
property var totpCoolDowns: []
2017-02-17 12:50:17 +03:00
SystemPalette { id: palette }
2017-02-07 16:17:54 +03:00
/*******
Settings
*******/
Settings {
id: settings
property bool slotMode
property bool slot1
property bool slot2
property var slot1digits
property var slot2digits
}
2017-02-17 12:50:17 +03:00
/*******
2017-02-07 16:17:54 +03:00
2017-02-17 12:50:17 +03:00
Main menu bar
2017-02-02 16:16:52 +03:00
2017-02-17 12:50:17 +03:00
*******/
2017-02-04 00:54:13 +03:00
menuBar: MenuBar {
Menu {
title: qsTr("File")
2017-02-03 11:58:17 +03:00
MenuItem {
2017-02-10 14:44:10 +03:00
text: qsTr('Add credential...')
onTriggered: {
addCredential.clear();
addCredential.open()
}
2017-02-16 12:04:35 +03:00
shortcut: StandardKey.New
2017-02-03 11:58:17 +03:00
}
2017-02-10 14:44:10 +03:00
MenuItem {
text: qsTr('Set password...')
enabled: !settings.slotMode
2017-02-10 14:44:10 +03:00
onTriggered: setPassword.open()
}
2017-02-17 10:52:34 +03:00
MenuItem {
text: qsTr('Reset...')
enabled: !settings.slotMode
2017-02-17 10:52:34 +03:00
onTriggered: reset.open()
}
2017-02-20 14:27:10 +03:00
MenuItem {
text: qsTr('Settings')
onTriggered: settingsDialog.open()
2017-02-20 14:27:10 +03:00
}
MenuItem {
text: qsTr("Exit")
onTriggered: Qt.quit()
2017-02-16 16:14:14 +03:00
shortcut: StandardKey.Quit
}
}
Menu {
title: qsTr("Help")
MenuItem {
2017-01-27 16:28:06 +03:00
text: qsTr("About Yubico Authenticator")
onTriggered: aboutPage.show()
}
}
}
AboutPage {
id: aboutPage
}
2017-02-03 11:58:17 +03:00
AddCredential {
id: addCredential
2017-02-24 15:52:44 +03:00
onAccepted: refreshDependingOnMode(true)
2017-02-03 11:58:17 +03:00
}
2017-02-20 14:27:10 +03:00
/*******
Settings dialog
*******/
SettingsDialog {
id: settingsDialog
settings: settings
onAccepted: {
settings.slotMode = settingsDialog.slotMode
settings.slot1 = settingsDialog.slot1
settings.slot2 = settingsDialog.slot2
settings.slot1digits = settingsDialog.slot1digits
settings.slot2digits = settingsDialog.slot2digits
2017-02-23 14:18:20 +03:00
refreshDependingOnMode(true)
}
2017-02-20 14:27:10 +03:00
}
2017-02-17 12:50:17 +03:00
/*******
Set password dialog
*******/
2017-02-10 14:44:10 +03:00
SetPassword {
id: setPassword
2017-02-16 17:22:20 +03:00
onAccepted: {
if (setPassword.newPassword !== setPassword.confirmPassword) {
noMatch.open()
} else {
if (setPassword.newPassword != "") {
device.setPassword(setPassword.newPassword)
} else {
device.setPassword(null)
}
passwordUpdated.open()
}
}
}
MessageDialog {
id: noMatch
icon: StandardIcon.Critical
title: qsTr("Passwords does not match")
text: qsTr("Password confirmation does not match password.")
standardButtons: StandardButton.Ok
onAccepted: setPassword.open()
2017-02-10 14:44:10 +03:00
}
2017-02-07 16:17:54 +03:00
2017-02-10 16:01:49 +03:00
MessageDialog {
id: passwordUpdated
icon: StandardIcon.Information
title: qsTr("Password set")
text: qsTr("A new password has been set.")
standardButtons: StandardButton.Ok
}
2017-02-17 12:50:17 +03:00
/*******
Reset dialog
*******/
2017-02-17 10:52:34 +03:00
MessageDialog {
id: reset
icon: StandardIcon.Critical
title: qsTr("Reset OATH functionality")
text: qsTr("This will delete all OATH credentials stored on the device, and reset the password. This action cannot be undone. Are you sure you want to reset the device?")
standardButtons: StandardButton.Ok | StandardButton.Cancel
onAccepted: {
device.reset()
device.refreshCredentials()
}
}
2017-02-17 12:50:17 +03:00
/*******
Device - prompt for password if needed
*******/
onHasDeviceChanged: {
if (device.hasDevice) {
2017-02-28 12:57:10 +03:00
if(!settings.slotMode && device.hasCCID) {
2017-02-23 12:40:24 +03:00
device.promptOrSkip(passwordPrompt)
}
2017-02-17 12:50:17 +03:00
} else {
passwordPrompt.close()
addCredential.close()
2017-02-28 12:57:10 +03:00
expiration = 0
2017-02-17 12:50:17 +03:00
}
2017-02-03 11:58:17 +03:00
}
2017-02-17 12:50:17 +03:00
PasswordPrompt {
id: passwordPrompt
}
onCredentialsChanged: {
updateExpiration()
2017-02-17 18:06:08 +03:00
hotpCoolDowns = []
totpCoolDowns = []
2017-02-17 16:09:55 +03:00
hotpTouchTimer.stop()
2017-02-17 18:06:08 +03:00
hotpCoolDownTimer.stop()
2017-02-17 12:50:17 +03:00
touchYourYubikey.close()
}
// @disable-check M301
YubiKey {
id: yk
onError: {
console.log(error)
}
onWrongPassword: {
passwordPrompt.open()
}
}
2017-02-17 12:50:17 +03:00
Text {
visible: !device.hasDevice
id: noLoadedDeviceMessage
text: if (device.nDevices == 0) {
qsTr("No YubiKey detected")
} else if (device.nDevices == 1) {
qsTr("Connecting to YubiKey...")
} else {
qsTr("Multiple YubiKeys detected!")
}
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: parent.width
2017-02-17 12:50:17 +03:00
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
2017-02-28 11:32:48 +03:00
Text {
visible: device.hasDevice
text: if (credentials !== null && filteredCredentials(credentials).length === 0) {
qsTr("No credentials found.")
} else if (settings.slotMode && !device.hasOTP) {
qsTr("Authenticator mode is set to YubiKey slots, but the OTP connection mode is not enabled.")
} else if (!settings.slotMode && !device.hasCCID) {
qsTr("Authenticator mode is set to CCID, but the CCID connection mode is not enabled.")
2017-02-28 11:32:48 +03:00
} else {
""
}
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: parent.width
2017-02-28 11:32:48 +03:00
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
2017-02-17 12:50:17 +03:00
2017-02-17 12:50:17 +03:00
/*******
Clipboard
*******/
TextEdit {
id: clipboard
visible: false
function setClipboard(value) {
text = value
selectAll()
copy()
2017-02-03 11:58:17 +03:00
}
2017-02-03 18:23:28 +03:00
}
2017-02-03 11:58:17 +03:00
2017-02-17 12:50:17 +03:00
/*******
Right click menu for credentials
*******/
Menu {
id: credentialMenu
2017-02-03 18:23:28 +03:00
MenuItem {
text: qsTr('Copy')
2017-02-16 11:55:47 +03:00
shortcut: StandardKey.Copy
onTriggered: {
2017-02-16 13:07:41 +03:00
if (repeater.selected != null) {
clipboard.setClipboard(repeater.selected.code)
2017-02-16 11:55:47 +03:00
}
}
2017-02-03 18:23:28 +03:00
}
MenuItem {
2017-02-17 16:09:55 +03:00
visible: allowManualGenerate(repeater.selected)
enabled: allowManualGenerate(repeater.selected) && !isInCoolDown(repeater.selected.name)
2017-02-03 18:23:28 +03:00
text: qsTr('Generate code')
2017-02-16 11:55:47 +03:00
shortcut: "Space"
2017-02-17 12:14:37 +03:00
onTriggered: {
if (!isInCoolDown(repeater.selected.name)) {
calculateCredential(repeater.selected)
if (repeater.selected.oath_type === "hotp") {
2017-02-17 16:09:55 +03:00
hotpCoolDowns.push(repeater.selected.name)
hotpCoolDownTimer.restart()
2017-02-17 18:06:08 +03:00
} else if(repeater.selected.touch) {
totpCoolDowns.push(repeater.selected.name)
2017-02-17 12:14:37 +03:00
}
}
}
2017-02-03 18:23:28 +03:00
}
MenuItem {
text: qsTr('Delete')
2017-02-16 11:55:47 +03:00
shortcut: StandardKey.Delete
2017-02-03 18:23:28 +03:00
onTriggered: confirmDeleteCredential.open()
}
2017-02-03 18:23:28 +03:00
}
2017-02-03 11:58:17 +03:00
2017-02-17 16:09:55 +03:00
function allowManualGenerate(cred) {
return cred != null && (cred.oath_type === "hotp" || repeater.selected.touch)
}
2017-02-17 12:50:17 +03:00
MessageDialog {
id: confirmDeleteCredential
icon: StandardIcon.Warning
title: qsTr("Delete credential?")
text: qsTr("Are you sure you want to delete the credential?")
standardButtons: StandardButton.Ok | StandardButton.Cancel
onAccepted: {
2017-02-23 18:00:17 +03:00
if (settings.slotMode){
device.deleteSlotCredential(getSlot(repeater.selected['name']))
} else {
device.deleteCredential(repeater.selected)
}
refreshDependingOnMode(true)
2017-02-17 12:50:17 +03:00
}
}
MessageDialog {
id: touchYourYubikey
icon: StandardIcon.Information
title: qsTr("Touch your YubiKey")
text: qsTr("Touch your YubiKey to generate the code.")
standardButtons: StandardButton.NoButton
2017-02-17 12:14:37 +03:00
}
2017-02-17 12:50:17 +03:00
/*******
Arrow key navigation
*******/
2017-02-16 13:07:41 +03:00
Item {
id: arrowKeys
focus: true
Keys.onUpPressed: {
2017-02-16 13:25:15 +03:00
if (repeater.selectedIndex == null) {
repeater.selected = repeater.model[repeater.model.length - 1]
repeater.selectedIndex = repeater.model.length - 1
} else if (repeater.selectedIndex > 0) {
2017-02-16 13:07:41 +03:00
repeater.selected = repeater.model[repeater.selectedIndex - 1]
repeater.selectedIndex = repeater.selectedIndex - 1
}
}
Keys.onDownPressed: {
2017-02-16 13:25:15 +03:00
if (repeater.selectedIndex == null) {
repeater.selected = repeater.model[0]
repeater.selectedIndex = 0
} else if (repeater.selectedIndex < repeater.model.length - 1) {
2017-02-16 13:07:41 +03:00
repeater.selected = repeater.model[repeater.selectedIndex + 1]
repeater.selectedIndex = repeater.selectedIndex + 1
}
}
}
2017-02-17 12:50:17 +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
spacing: 0
2017-02-17 12:50:17 +03:00
/*******
Time left bar
*******/
2017-02-06 17:00:11 +03:00
ProgressBar {
id: progressBar
2017-02-09 14:45:04 +03:00
visible: hasDevice
2017-02-06 17:00:11 +03:00
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Layout.maximumHeight: 10
Layout.minimumHeight: 10
Layout.minimumWidth: 300
Layout.fillWidth: true
maximumValue: 30
minimumValue: 0
style: ProgressBarStyle {
progress: Rectangle {
color: "#9aca3c"
}
2017-02-02 16:16:52 +03:00
2017-02-06 17:00:11 +03:00
background: Rectangle {
color: palette.mid
2017-02-02 16:16:52 +03:00
}
2017-02-06 15:42:17 +03:00
}
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
spacing: 0
2017-02-07 12:12:22 +03:00
visible: device.hasDevice
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
2017-02-17 12:50:17 +03:00
/*******
Credential items
*******/
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-02-16 13:07:41 +03:00
property var selected
property var selectedIndex
2017-02-06 17:00:11 +03:00
Rectangle {
id: credentialRectangle
2017-02-16 13:07:41 +03:00
focus: true
color: {
2017-02-16 13:07:41 +03:00
if (repeater.selected != null) {
if (repeater.selected.name == modelData.name) {
return palette.dark
}
}
if (index % 2 == 0) {
return "#00000000"
}
return palette.alternateBase
}
2017-02-06 17:00:11 +03:00
Layout.fillWidth: true
Layout.minimumHeight: 70
Layout.alignment: Qt.AlignTop
MouseArea {
anchors.fill: parent
onClicked: {
2017-02-16 13:07:41 +03:00
arrowKeys.forceActiveFocus()
if (mouse.button & Qt.LeftButton) {
2017-02-16 13:07:41 +03:00
if (repeater.selected != null
&& repeater.selected.name == modelData.name) {
repeater.selected = null
repeater.selectedIndex = null
} else {
2017-02-16 13:07:41 +03:00
repeater.selected = modelData
repeater.selectedIndex = index
}
}
if (mouse.button & Qt.RightButton) {
2017-02-16 13:07:41 +03:00
repeater.selected = modelData
repeater.selectedIndex = index
credentialMenu.popup()
}
2017-02-06 17:00:11 +03:00
}
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
spacing: -15
anchors.fill: parent
Text {
visible: hasIssuer(modelData.name)
text: qsTr('') + parseIssuer(modelData.name)
font.pointSize: 13
}
2017-02-15 17:14:34 +03:00
Text {
2017-02-17 12:14:37 +03:00
opacity: isInCoolDown(modelData.name) ? 0.6 : 1
2017-02-06 17:00:11 +03:00
visible: modelData.code != null
text: qsTr('') + modelData.code
font.family: "Verdana"
font.pointSize: 22
}
Text {
text: hasIssuer(
modelData.name) ? qsTr(
'') + parseName(
modelData.name) : modelData.name
font.pointSize: 13
}
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-23 12:40:24 +03:00
Text {
visible: settings.slotMode
text: "[Slot mode]"
}
2017-02-17 12:50:17 +03:00
/*******
Search field
*******/
2017-02-06 17:07:43 +03:00
TextField {
id: search
2017-02-09 14:45:04 +03:00
visible: hasDevice
2017-02-06 17:07:43 +03:00
placeholderText: 'Search...'
Layout.fillWidth: true
2017-02-06 17:00:11 +03:00
}
2017-02-02 16:16:52 +03:00
}
2017-02-06 17:57:32 +03:00
2017-02-02 16:16:52 +03:00
2017-02-17 12:50:17 +03:00
/*******
2017-02-03 18:23:28 +03:00
2017-02-17 12:50:17 +03:00
Timers
*******/
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 {
id: progressBarTimer
interval: 100
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
var timeLeft = expiration - (Date.now() / 1000)
if (timeLeft <= 0 && progressBar.value > 0) {
2017-02-23 14:18:20 +03:00
device.refresh(refreshDependingOnMode)
2017-02-17 18:06:08 +03:00
totpCoolDowns = []
2017-02-02 16:16:52 +03:00
}
progressBar.value = timeLeft
}
}
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-02-17 12:50:17 +03:00
/*******
Utility functions
*******/
2017-02-23 14:18:20 +03:00
function refreshDependingOnMode(force) {
if (hasDevice) {
2017-02-28 12:57:10 +03:00
if (settings.slotMode && device.hasOTP) {
2017-02-23 14:18:20 +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 = []
if (creds != null) {
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-02-17 16:09:55 +03:00
return hotpCoolDowns.indexOf(name) !== -1 || totpCoolDowns.indexOf(name) !== -1
2017-02-17 12:14:37 +03:00
}
2017-02-02 17:44:49 +03:00
function hasIssuer(name) {
return name.indexOf(':') !== -1
}
function parseName(name) {
return name.split(":").slice(1).join(":")
}
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-02-17 16:09:55 +03:00
if (credential.oath_type === 'hotp') {
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) {
return getSlotDigitsSettings()[slot -1]
}
2017-02-02 16:16:52 +03:00
function updateExpiration() {
var maxExpiration = 0
if (credentials !== null) {
for (var i = 0; i < credentials.length; i++) {
var exp = credentials[i].expiration
if (exp !== null && exp > maxExpiration) {
maxExpiration = exp
}
}
expiration = maxExpiration
}
}
}