yubioath-flutter/qml/Main.qml

590 lines
19 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-01 13:57:40 +03:00
title: settings.slotMode ? qsTr("Yubico Authenticator [Slot mode]") : qsTr(
"Yubico Authenticator")
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 validated: device.validated
property bool hasDevice: device.hasDevice
2017-03-01 13:57:40 +03:00
property bool canShowCredentials: hasDevice && ((settings.slotMode
&& device.hasOTP)
|| (!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-02-28 14:08:42 +03:00
SystemPalette {
id: palette
}
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
}
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: {
if (settings.slotMode) {
addCredentialSlot.clear()
2017-03-08 18:07:12 +03:00
device.getSlotStatus(addCredentialSlot.open)
} else {
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
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: {
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-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 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()
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
onHasDeviceChanged: {
if (device.hasDevice) {
2017-02-28 14:08:42 +03:00
if (!settings.slotMode && device.hasCCID) {
2017-03-01 13:57:40 +03:00
device.promptOrSkip(passwordPrompt, settings.savedPasswords)
2017-02-23 12:40:24 +03:00
}
2017-02-17 12:50:17 +03:00
} else {
passwordPrompt.close()
addCredential.close()
}
2017-02-03 11:58:17 +03:00
}
2017-02-17 12:50:17 +03:00
PasswordPrompt {
id: passwordPrompt
2017-03-01 13:57:40 +03:00
onAccepted: {
if (passwordPrompt.remember) {
device.validate(passwordPrompt.password, rememberPassword)
} else {
device.validate(passwordPrompt.password)
}
}
}
function rememberPassword() {
var deviceId = device.oathId
settings.savedPasswords += deviceId + ':' + device.passwordKey + ';'
2017-02-17 12:50:17 +03:00
}
onCredentialsChanged: {
2017-02-17 16:09:55 +03:00
hotpTouchTimer.stop()
2017-02-17 12:50:17 +03:00
touchYourYubikey.close()
}
// @disable-check M301
YubiKey {
id: yk
onError: {
console.log(error)
}
onWrongPassword: {
passwordPrompt.open()
}
}
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
2017-02-28 14:08:42 +03:00
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-03-01 13:57:40 +03:00
} else if (credentials == null) {
2017-02-28 14:30:02 +03:00
qsTr("Reading credentials...")
2017-02-28 11:32:48 +03:00
} else {
2017-03-01 13:57:40 +03:00
""
2017-02-28 11:32:48 +03:00
}
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
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
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)
2017-03-08 16:41:56 +03:00
enabled: allowManualGenerate(repeater.selected)
&& enableManualGenerate(repeater.selected)
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 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) {
2017-02-28 14:08:42 +03:00
return cred != null && (cred.oath_type === "hotp"
|| repeater.selected.touch)
2017-02-17 16:09:55 +03:00
}
2017-03-08 16:41:56 +03:00
function enableManualGenerate(cred) {
if (cred.oath_type !== 'hotp') {
return cred.code == null || isExpired(repeater.selected)
} else {
return !isInCoolDown(cred.name)
}
}
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-28 14:08:42 +03:00
if (settings.slotMode) {
2017-02-23 18:00:17 +03:00
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-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-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
ProgressBar {
id: progressBar
visible: canShowCredentials
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 {
2017-03-07 15:56:40 +03:00
color: palette.alternateBase
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
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
2017-03-07 13:58:08 +03:00
anchors.topMargin: 5
anchors.bottomMargin: 5
spacing: 0
2017-02-06 17:00:11 +03:00
anchors.fill: parent
Text {
visible: hasIssuer(modelData.name)
text: qsTr('') + parseIssuer(modelData.name)
2017-03-07 13:58:08 +03:00
font.pointSize: 10
2017-02-06 17:00:11 +03:00
}
2017-02-15 17:14:34 +03:00
Text {
opacity: isExpired(modelData) ? 0.6 : 1
2017-02-06 17:00:11 +03:00
visible: modelData.code != null
text: qsTr('') + modelData.code
font.family: "Verdana"
2017-03-07 13:58:08 +03:00
font.pointSize: 16
2017-02-06 17:00:11 +03:00
}
Text {
text: hasIssuer(
modelData.name) ? qsTr(
'') + parseName(
modelData.name) : modelData.name
2017-03-07 13:58:08 +03:00
font.pointSize: 10
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
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
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: {
2017-02-28 15:20:44 +03:00
var timeLeft = device.expiration - (Date.now() / 1000)
2017-02-02 16:16:52 +03:00
if (timeLeft <= 0 && progressBar.value > 0) {
refreshDependingOnMode(true)
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()
}
function isExpired(cred) {
2017-03-08 16:41:56 +03:00
return cred != null && (cred.oath_type !== 'hotp')
&& (cred.expiration - (Date.now() / 1000) <= 0)
}
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 = []
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-03-08 16:50:50 +03:00
return hotpCoolDowns.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) {
2017-02-28 14:08:42 +03:00
return getSlotDigitsSettings()[slot - 1]
}
}