2017-03-07 13:58:08 +03:00
import QtQuick 2.5
2017-01-27 15:55:38 +03:00
import QtQuick . Controls 1.4
import QtQuick . Layouts 1.1
2017-02-02 16:16:52 +03:00
import QtQuick . Controls . Styles 1.4
2017-01-27 15:55:38 +03:00
import QtQuick . Dialogs 1.2
2017-02-20 15:47:39 +03:00
import Qt . labs . settings 1.0
2017-01-27 15:55:38 +03:00
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
2017-01-27 15:55:38 +03:00
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
}
2017-02-20 15:47:39 +03:00
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
2017-02-20 15:47:39 +03:00
}
2017-01-27 15:55:38 +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...' )
2017-02-20 11:14:05 +03:00
onTriggered: {
2017-03-07 17:16:10 +03:00
if ( settings . slotMode ) {
addCredentialSlot . clear ( )
2017-03-08 18:07:12 +03:00
device . getSlotStatus ( addCredentialSlot . open )
2017-03-07 17:16:10 +03:00
} else {
addCredential . clear ( )
addCredential . open ( )
}
2017-02-20 11:14:05 +03:00
}
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...' )
2017-02-23 16:42:51 +03:00
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...' )
2017-02-23 16:42:51 +03:00
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' )
2017-02-20 15:47:39 +03:00
onTriggered: settingsDialog . open ( )
2017-02-20 14:27:10 +03:00
}
2017-01-27 15:55:38 +03:00
MenuItem {
text: qsTr ( "Exit" )
onTriggered: Qt . quit ( )
2017-02-16 16:14:14 +03:00
shortcut: StandardKey . Quit
2017-01-27 15:55:38 +03:00
}
}
Menu {
title: qsTr ( "Help" )
MenuItem {
2017-01-27 16:28:06 +03:00
text: qsTr ( "About Yubico Authenticator" )
2017-01-27 15:55:38 +03:00
onTriggered: aboutPage . show ( )
}
}
}
AboutPage {
id: aboutPage
}
2017-02-03 11:58:17 +03:00
AddCredential {
id: addCredential
2017-03-07 17:16:10 +03:00
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
}
2017-02-20 15:47:39 +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 15:47:39 +03:00
}
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!" )
}
2017-02-28 12:04:46 +03:00
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." )
2017-02-28 12:04:46 +03:00
} 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
}
2017-02-28 12:04:46 +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
2017-02-03 15:58:22 +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 15:58:22 +03:00
}
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
2017-02-28 14:29:23 +03:00
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
2017-02-16 11:18:52 +03:00
color: {
2017-02-16 13:07:41 +03:00
if ( repeater . selected != null ) {
if ( repeater . selected . name == modelData . name ) {
2017-02-16 11:18:52 +03:00
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 ( )
2017-02-16 11:18:52 +03:00
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
2017-02-16 11:18:52 +03:00
} else {
2017-02-16 13:07:41 +03:00
repeater . selected = modelData
repeater . selectedIndex = index
2017-02-16 11:18:52 +03:00
}
}
if ( mouse . button & Qt . RightButton ) {
2017-02-16 13:07:41 +03:00
repeater . selected = modelData
repeater . selectedIndex = index
2017-02-16 11:18:52 +03:00
credentialMenu . popup ( )
}
2017-02-06 17:00:11 +03:00
}
2017-02-16 11:18:52 +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 {
2017-03-08 15:58:42 +03:00
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
2017-02-28 14:29:23 +03:00
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
2017-01-27 15:55:38 +03:00
Timer {
2017-02-02 16:16:52 +03:00
id: ykTimer
2017-01-27 15:55:38 +03:00
triggeredOnStart: true
interval: 500
repeat: true
running: true
2017-02-23 12:40:24 +03:00
onTriggered: device . refresh ( refreshDependingOnMode )
2017-01-27 15:55:38 +03:00
}
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 ) {
2017-03-08 15:58:42 +03:00
refreshDependingOnMode ( true )
2017-02-02 16:16:52 +03:00
}
progressBar . value = timeLeft
}
2017-01-27 15:55:38 +03:00
}
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-01-27 15:55:38 +03:00
}
2017-03-08 15:58:42 +03:00
function isExpired ( cred ) {
2017-03-08 16:41:56 +03:00
return cred != null && ( cred . oath_type !== 'hotp' )
2017-03-08 15:58:42 +03:00
&& ( 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 ) {
2017-02-23 15:51:44 +03:00
if ( settings . slotMode ) {
2017-02-23 18:00:17 +03:00
var slot = getSlot ( credential . name )
2017-02-23 15:51:44 +03:00
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 ) {
2017-02-23 15:51:44 +03:00
return 1
}
2017-02-23 18:00:17 +03:00
if ( name . indexOf ( '2' ) !== - 1 ) {
2017-02-23 15:51:44 +03:00
return 2
}
}
function getDigits ( slot ) {
2017-02-28 14:08:42 +03:00
return getSlotDigitsSettings ( ) [ slot - 1 ]
2017-02-23 15:51:44 +03:00
}
2017-01-27 15:55:38 +03:00
}