diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml index b63513e..85faee5 100644 --- a/qml/pages/FirstPage.qml +++ b/qml/pages/FirstPage.qml @@ -9,11 +9,12 @@ Page { property var refresh_issued property ListModel keysList: ListModel{} property int editorStyle: TextEditor.UnderlineBackground + property bool allow_deletion: false - property var keyName; - property var secret; - property string hashAlgo: 'SHA1'; - property string issuer: ''; + property var keyName + property var secret + property string hashAlgo: 'SHA1' + property string issuer: '' // The effective value will be restricted by ApplicationWindow.allowedOrientations allowedOrientations: Orientation.All @@ -21,27 +22,41 @@ Page { onStatusChanged: { if ((status == Component.Ready)) { + refresh_issued = true; python.getKeys(); - refresh_issued = false; } } // To enable PullDownMenu, place our content in a SilicaFlickable - SilicaFlickable { + property bool deletingItems + + SilicaListView { + id: listView + anchors.fill: parent + model: keysList + + header: PageHeader { + title: qsTr("YUBIKEY OATH TOTP Keys") + } + + /* + ViewPlaceholder { + enabled: (keysList.populated && keysList.count === 0) + text: "No Keys" + hintText: "YubiKey not connected or no OATH Keys?" + } + */ // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView PullDownMenu { -// MenuItem { -// text: qsTr("Show Page 2") -// onClicked: { -// console.log(new Date().getTime()) -// console.log(keys[0]['code']['valid_to']*1000); -// console.log(keys[0]['code']['valid_to']*1000 - new Date().getTime()); -// console.log((keys[0]['code']['valid_to'] - keys[0]['code']['valid_from'])); -// console.log((keys[0]['code']['valid_to'] - new Date().getTime()/1000)/ (keys[0]['code']['valid_to'] - keys[0]['code']['valid_from'])); -// } -// } + MenuItem { + text: qsTr("Toggel allow key deletion") + onClicked: { + if (allow_deletion === true) allow_deletion = false + else allow_deletion = true + } + } MenuItem { text: qsTr("Add Key") onClicked: { @@ -61,99 +76,99 @@ Page { } - // Tell SilicaFlickable the height of its content. - contentHeight: column.height + delegate: ListItem { + function remove() { + remorseDelete(function() { + refresh_issued = true; + python.call('ykcon.ykcon.deleteKey', [model.cred['id']], function() {}); + python.getKeys(); + }) - // Place our content in a Column. The PageHeader is always placed at the top - // of the page, followed by our content. - Column { - id: column + } - width: page.width - spacing: Theme.paddingLarge - PageHeader { - title: qsTr("YUBIKEY OATH TOTP Keys") + onClicked: { + if (!menuOpen && pageStack.depth == 2) { + pageStack.animatorPush(Qt.resolvedUrl("ListPage.qml")) + } } - ProgressBar { - id: codeElapsed + ListView.onRemove: animateRemoval() + opacity: enabled ? 1.0 : 0.0 + Behavior on opacity { FadeAnimator {}} + + menu: Component { + ContextMenu { + MenuItem { + text: qsTr("To clipboard") + onClicked: Clipboard.text = model.code['value'] + } + MenuItem { + enabled: allow_deletion + text: qsTr("Delete") + onClicked: remove() + } + } + } + Row { + width: parent.width + spacing: Theme.paddingMedium + anchors { left: parent.left right: parent.right - margins: Theme.paddingSmall + margins: Theme.paddingLarge } - //width: Theme.iconSizeMedium - //height: Theme.iconSizeMedium - width: parent.width - minimumValue: 0 - maximumValue: 100 - //valueText: value - //label: "Progress" - Timer { - id: refresh_timer - interval: 100 - repeat: true - onTriggered: { - try{ - codeElapsed.value = 100*((keys[0]['code']['valid_to'] - new Date().getTime()/1000)/ (keys[0]['code']['valid_to'] - keys[0]['code']['valid_from'])); - if (codeElapsed.value <= 0 && refresh_issued === false) { - refresh_issued = true; - python.getKeys(); - } - } catch (e) { - console.log('error?') - codeElapsed = 0; - python.getKeys(); - } - } - running: Qt.application.active + + Label { + text: model.cred['id'] + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + width: parent.width * 3 / 4 + } + Label { + text: model.code['value'] + font.pixelSize: Theme.fontSizeSmall + font.bold: true + wrapMode: Text.Wrap + width: parent.width * 1 / 4 } } + } + VerticalScrollDecorator {} + + ProgressBar { + id: codeElapsed + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + margins: Theme.paddingSmall + } - Repeater { - - model: keysList - - Row { - width: parent.width - spacing: Theme.paddingMedium - - anchors { - left: parent.left - right: parent.right - margins: Theme.paddingLarge - } - - Label { - text: model.cred['id'] - color: Theme.secondaryHighlightColor - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - width: parent.width * 3 / 4 + width: parent.width + minimumValue: 0 + maximumValue: 100 + + Timer { + id: refresh_timer + interval: 100 + repeat: true + onTriggered: { + try{ + codeElapsed.value = 100*((keys[0]['code']['valid_to'] - new Date().getTime()/1000)/ (keys[0]['code']['valid_to'] - keys[0]['code']['valid_from'])); + if (codeElapsed.value <= 0 && refresh_issued === false) { + refresh_issued = true; + python.getKeys(); } - Label { - text: model.code['value'] - color: Theme.secondaryHighlightColor - font.pixelSize: Theme.fontSizeSmall - font.bold: true - wrapMode: Text.Wrap - width: parent.width * 1 / 4 + } catch (e) { + codeElapsed.value = 0; + python.getKeys(); } } + running: Qt.application.active } - - -// Label { -// id: label1 -// x: Theme.horizontalPageMargin -// text: qsTr("Hello Sailors") -// color: Theme.secondaryHighlightColor -// font.pixelSize: Theme.fontSizeSmall -// wrapMode: Text.Wrap -// width: parent.width -// } - } + } Python { @@ -162,9 +177,8 @@ Page { addImportPath(Qt.resolvedUrl('.')); setHandler('keys', function(val) { - //label1.text = val - keysList.clear(); keys = JSON.parse(val); + keysList.clear(); for (var s_key in keys) { keysList.append({'cred': keys[s_key]['cred'], 'code': keys[s_key]['code']}) } @@ -173,9 +187,19 @@ Page { }); setHandler('no_key', function(val) { - //label1.text = val refresh_timer.stop() + }); + setHandler('del:key_not_found', function(val) { + console.log("del:key_not_found") + }); + + setHandler('del:succ', function(val) { + console.log("del:succ") + }); + + setHandler('del:not_unique', function(val) { + console.log("del:not_unique") }); importModule('ykcon', function () {}); @@ -199,107 +223,79 @@ Page { } Component { - id: newKeyDialog - Dialog { - - onAccepted: { - - keyName = nameField.text - secret = secretField.text - hashAlgo = cbxHashAlgo.currentItem.text - issuer = issuerField.text - - refresh_issued = true; - python.call('ykcon.ykcon.writeKey', [keyName, secret, hashAlgo, issuer], function() {}); - python.getKeys(); - - // pageStack.replace(newKeyDialog_page2) - + id: newKeyDialog + Dialog { + + onAccepted: { + keyName = nameField.text + secret = secretField.text + hashAlgo = cbxHashAlgo.currentItem.text + issuer = issuerField.text + + refresh_issued = true; + python.call('ykcon.ykcon.writeKey', [keyName, secret, hashAlgo, issuer], function() {}); + python.getKeys(); + } + + Column { + id: column + width: parent.width + + DialogHeader { + id: header + title: qsTr("Add OATH TOTP Key") } - Column { - id: column - width: parent.width - - DialogHeader { - id: header - title: "Add OATH TOTP Key" - } - - SectionHeader { - text: "Credential details" - } - - TextField { - id: nameField - focus: true - label: "Name" - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: secretField.focus = true - - backgroundStyle: page.editorStyle - } + SectionHeader { + text: qsTr("Credential details") + } - TextField { - id: secretField - focus: true - label: "Secret" - EnterKey.iconSource: "image://theme/icon-m-enter-next" - EnterKey.onClicked: issuerField.focus = true + TextField { + id: nameField + focus: true + label: qsTr("Name") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: secretField.focus = true - backgroundStyle: page.editorStyle - } - - SectionHeader { - text: "Advanced (optional)" - } - - TextField { - id: issuerField - focus: true - label: "Issuer" - EnterKey.iconSource: "image://theme/icon-m-enter-close" - EnterKey.onClicked: focus = false + backgroundStyle: page.editorStyle + } - backgroundStyle: page.editorStyle - } + TextField { + id: secretField + focus: true + label: qsTr("Secret") + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: issuerField.focus = true - ComboBox { - id: cbxHashAlgo - label: "Hash Algorythm:" - width: parent.width + backgroundStyle: page.editorStyle + } - menu: ContextMenu { - MenuItem { text: "SHA1" } - MenuItem { text: "SHA256" } - MenuItem { text: "SHA512" } - } - } - } - } - } + SectionHeader { + text: qsTr("Advanced (optional)") + } - /* - Component { - id: newKeyDialog_page2 - Dialog { + TextField { + id: issuerField + focus: true + label: qsTr("Issuer") + EnterKey.iconSource: "image://theme/icon-m-enter-close" + EnterKey.onClicked: focus = false - onAccepted: { - refresh_issued = true; - python.call('ykcon.ykcon.writeKey', [keyName, secret, hashAlgo, issuer], function() {}); - python.getKeys(); + backgroundStyle: page.editorStyle } - Column { - id: column + ComboBox { + id: cbxHashAlgo + label: qsTr("Hash Algorythm:") width: parent.width - DialogHeader { - id: header - title: "Plug Yubikey now and continue." + menu: ContextMenu { + MenuItem { text: "SHA1" } + MenuItem { text: "SHA256" } + MenuItem { text: "SHA512" } } - } + } } } - */ + } } diff --git a/qml/pages/ykcon.py b/qml/pages/ykcon.py index 7a74301..d4a882c 100644 --- a/qml/pages/ykcon.py +++ b/qml/pages/ykcon.py @@ -1,6 +1,7 @@ # This Python file uses the following encoding: utf-8 # Some of the code may be taken from https://github.com/Yubico/yubioath-desktop/blob/main/py/yubikey.py +# and https://github.com/Yubico/yubikey-manager/blob/main/ykman/cli/oath.py import pyotherside import json @@ -24,12 +25,14 @@ def __init__(self): pass def getKeys(self): - could_register = False - for device, info in list_all_devices(): - if info.version >= (5, 0, 0): # The info object provides details about the YubiKey - connection, _, _ = connect_to_device(serial=info.serial, connection_types=[SmartCardConnection]) + # The info object provides details about the YubiKey + if info.version >= (5, 0, 0): + connection, _, _ = connect_to_device( + serial=info.serial, + connection_types=[SmartCardConnection] + ) with connection: could_register = True mySession = OathSession(connection) @@ -38,22 +41,32 @@ def getKeys(self): for key in entries: cred = key code = entries[key] - code_dct = {'cred':cred, - 'code' :code + code_dct = {'cred': cred, + 'code': code } codes.append(code_dct) if len(codes) > 0: - pyotherside.send('keys', json.dumps(codes, default=dumper, indent=2)) + pyotherside.send( + 'keys', + json.dumps( + codes, + default=dumper, + indent=2 + ) + ) else: pyotherside.send('no_key') if not could_register: pyotherside.send('no_key') def writeKey(self, name, secret, hash_algo, issuer='', digits=6): - - new_credentials = CredentialData(name, OATH_TYPE.TOTP, HASH_ALGORITHM.SHA1, parse_b32_key(secret)) - + new_credentials = CredentialData( + name, + OATH_TYPE.TOTP, + HASH_ALGORITHM.SHA1, + parse_b32_key(secret) + ) if '256' in hash_algo: new_credentials.hash_algorithm = HASH_ALGORITHM.SHA256 if '512' in hash_algo: @@ -62,19 +75,60 @@ def writeKey(self, name, secret, hash_algo, issuer='', digits=6): new_credentials.digits = int(digits) for device, info in list_all_devices(): - if info.version >= (5, 0, 0): # The info object provides details about the YubiKey - connection, _, _ = connect_to_device(serial=info.serial, connection_types=[SmartCardConnection]) + # The info object provides details about the YubiKey + if info.version >= (5, 0, 0): + connection, _, _ = connect_to_device( + serial=info.serial, + connection_types=[SmartCardConnection] + ) with connection: mySession = OathSession(connection) mySession.put_credential(credential_data=new_credentials) + def deleteKey(self, name): + for device, info in list_all_devices(): + # The info object provides details about the YubiKey + if info.version >= (5, 0, 0): + connection, _, _ = connect_to_device( + serial=info.serial, + connection_types=[SmartCardConnection] + ) + with connection: + could_register = True + mySession = OathSession(connection) + creds = mySession.list_credentials() + hits = _search(creds, name, True) + if len(hits) == 0: + pyotherside.send("del:key_not_found") + elif len(hits) == 1: + cred = hits[0] + mySession.delete_credential(cred.id) + pyotherside.send("del:succ") + else: + pyotherside.send("del:not_unique") + def dumper(obj): """JSON serialization of bytes""" if isinstance(obj, bytes): return obj.decode() try: return obj.toJSON() - except: # pylint: disable=bare-except + except: return obj.__dict__ +def _search(creds, query, show_hidden): + hits = [] + for c in creds: + cred_id = _string_id(c) + # if not show_hidden and is_hidden(c): + # continue + if cred_id == query: + return [c] + if query.lower() in cred_id.lower(): + hits.append(c) + return hits + +def _string_id(credential): + return credential.id.decode("utf-8") + ykcon = Ykcon() diff --git a/rpm/harbour-yubigo.spec b/rpm/harbour-yubigo.spec index 81cdbd9..a74b410 100644 --- a/rpm/harbour-yubigo.spec +++ b/rpm/harbour-yubigo.spec @@ -9,8 +9,8 @@ Name: harbour-yubigo # << macros Summary: ykman GUI for SFOS -Version: 0.3 -Release: 3 +Version: 0.4 +Release: 1 Group: Qt/Qt License: GPL3 URL: https://github.com/fridlmue/harbour-yubigo diff --git a/rpm/harbour-yubigo.yaml b/rpm/harbour-yubigo.yaml index 056c65f..912dd6a 100644 --- a/rpm/harbour-yubigo.yaml +++ b/rpm/harbour-yubigo.yaml @@ -1,7 +1,7 @@ Name: harbour-yubigo Summary: ykman GUI for SFOS -Version: 0.3 -Release: 3 +Version: 0.4 +Release: 1 # The contents of the Group field should be one of the groups listed here: # https://github.com/mer-tools/spectacle/blob/master/data/GROUPS Group: Qt/Qt diff --git a/translations/harbour-yubigo-de.ts b/translations/harbour-yubigo-de.ts index 387ce33..e02030e 100644 --- a/translations/harbour-yubigo-de.ts +++ b/translations/harbour-yubigo-de.ts @@ -10,6 +10,10 @@ FirstPage + + YUBIKEY OATH TOTP Keys + + Add Key @@ -19,7 +23,43 @@ - YUBIKEY OATH TOTP Keys + Toggel allow key deletion + + + + To clipboard + + + + Delete + + + + Add OATH TOTP Key + + + + Credential details + + + + Name + + + + Secret + + + + Advanced (optional) + + + + Issuer + + + + Hash Algorythm: diff --git a/translations/harbour-yubigo.ts b/translations/harbour-yubigo.ts index 387ce33..e02030e 100644 --- a/translations/harbour-yubigo.ts +++ b/translations/harbour-yubigo.ts @@ -10,6 +10,10 @@ FirstPage + + YUBIKEY OATH TOTP Keys + + Add Key @@ -19,7 +23,43 @@ - YUBIKEY OATH TOTP Keys + Toggel allow key deletion + + + + To clipboard + + + + Delete + + + + Add OATH TOTP Key + + + + Credential details + + + + Name + + + + Secret + + + + Advanced (optional) + + + + Issuer + + + + Hash Algorythm: