From 631868ddba5fda97385ca0ddb086a6b0a2a04d61 Mon Sep 17 00:00:00 2001 From: Stefan Brand Date: Wed, 8 Jan 2014 19:28:06 +0100 Subject: [PATCH 1/4] Version 0.2 * Added possibility to edit existing entries * Better error handling for invalid secrets * Better input checks on add/edit --- COPYING | 26 +++++ README.md | 36 ++++++ qml/cover/CoverPage.qml | 27 +++-- qml/harbour-sailotp.qml | 4 +- qml/lib/crypto.js | 63 +++++++---- qml/lib/storage.js | 67 ++++++----- qml/pages/About.qml | 98 ++++++++-------- qml/pages/AddOTP.qml | 89 +++++++++------ qml/pages/MainView.qml | 238 +++++++++++++++++++++------------------ rpm/harbour-sailotp.spec | 2 +- rpm/harbour-sailotp.yaml | 2 +- src/harbour-sailotp.cpp | 58 +++++----- 12 files changed, 415 insertions(+), 295 deletions(-) create mode 100644 COPYING create mode 100644 README.md diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..c568412 --- /dev/null +++ b/COPYING @@ -0,0 +1,26 @@ +Copyright (c) 2013, Stefan Brand +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + +3. The names of the contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dddbb0c --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# SailOTP + +SailOTP is a Sailfish Implementation of the Google-Authenticator algorithm, +also known as Timebased One Time Pad (TOPT) as described in RFC 6238. A growing +number of sites uses this algorithm for two-factor-authentication, including +Github, Linode and several Google services. + +At the moment the App is quite basic. One can add new OTP-entries using the +pulley-menu. The main view of the app will show a List off all entries and +their current One-Time-Tokens. The entries will be regenerated every 30 seconds, the remaining time for the current tokens is shown through a progress bar at the top of the app. An entry can be deleted by long-pressing on it. + +## Known Limitations + +At the moment the only way to insert new entries into the app is to insert the +title and secret key by hand. It's not possible to use the QR-Codes some sites +provide directly. + +## Contact and Issues + +If you find any bugs or want to suggest a feature, feel free to use Githubs +Issues feature. + +## License + +SailOTP is licensed under a 3-Clause BSD-License. See COPYING for details. + +## Accnowledgements + +SailOTP uses the SHA-1 and HMAC-Implementation from + +https://github.com/Caligatio/jsSHA + +The implementation of the TOTP-algorithm was inspired by: + +http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/ + diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 07e83ab..e307c99 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -30,19 +30,22 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +// Define the Layout of the Active Cover CoverBackground { - Image { - id: logo - source: "../sailotp.png" - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 48 - } + // Show the SailOTP Logo + Image { + id: logo + source: "../sailotp.png" + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 48 + } - Label { - id: label - anchors.centerIn: parent - text: "SailOTP" - } + // Show the Application Name + Label { + id: label + anchors.centerIn: parent + text: "SailOTP" + } } diff --git a/qml/harbour-sailotp.qml b/qml/harbour-sailotp.qml index 260a2f3..b8632dc 100644 --- a/qml/harbour-sailotp.qml +++ b/qml/harbour-sailotp.qml @@ -33,8 +33,8 @@ import "pages" ApplicationWindow { - initialPage: Component { MainView { } } - cover: Qt.resolvedUrl("cover/CoverPage.qml") + initialPage: Component { MainView { } } + cover: Qt.resolvedUrl("cover/CoverPage.qml") } diff --git a/qml/lib/crypto.js b/qml/lib/crypto.js index 2d96a78..d518466 100644 --- a/qml/lib/crypto.js +++ b/qml/lib/crypto.js @@ -29,49 +29,66 @@ .import "./sha.js" as SHA -// Helper Functions +// *** Helper Functions *** // + +// Decimal to HEX function dec2hex(s) { return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); } +// HEX to Decimal function hex2dec(s) { return parseInt(s, 16); } - +// Convert Base32-secret to HEX Value function base32tohex(base32) { - var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - var bits = ""; - var hex = ""; + var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + var bits = ""; + var hex = ""; - for (var i = 0; i < base32.length; i++) { - var val = base32chars.indexOf(base32.charAt(i).toUpperCase()); - bits += leftpad(val.toString(2), 5, '0'); - } - - for (var i = 0; i+4 <= bits.length; i+=4) { - var chunk = bits.substr(i, 4); - hex = hex + parseInt(chunk, 2).toString(16) ; - } - return hex; + for (var i = 0; i < base32.length; i++) { + var val = base32chars.indexOf(base32.charAt(i).toUpperCase()); + bits += leftpad(val.toString(2), 5, '0'); + } + for (var i = 0; i+4 <= bits.length; i+=4) { + var chunk = bits.substr(i, 4); + hex = hex + parseInt(chunk, 2).toString(16) ; + } + return hex; } +// Pad Strings to given length function leftpad(str, len, pad) { - if (len + 1 >= str.length) { - str = Array(len + 1 - str.length).join(pad) + str; - } - return str; + if (len + 1 >= str.length) { + str = Array(len + 1 - str.length).join(pad) + str; + } + return str; } +// *** Main Function *** // + // Calculate an OTP-Value from the given secret +// Parameter is the secret key in Base32-notation function calcOTP(secret) { - var key = base32tohex(secret); - var epoch = Math.round(new Date().getTime() / 1000.0); - var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); + // Convert the key to HEX + var key = base32tohex(secret); + // Get current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC) + var epoch = Math.round(new Date().getTime() / 1000.0); + // Get last full 30 / 60 Seconds and convert to HEX + var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); + try { + // Calculate the SHA-1 HMAC Value from time and key var hmacObj = new SHA.jsSHA(time, 'HEX'); var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX"); + // Finally convert the HMAC-Value to the corresponding 6-digit token var offset = hex2dec(hmac.substring(hmac.length - 1)); var otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec('7fffffff')) + ''; otp = (otp).substr(otp.length - 6, 6); - return otp; + } catch (e) { + otp = "Invalid Secret!" + } + + // return the calculated token + return otp; } diff --git a/qml/lib/storage.js b/qml/lib/storage.js index b6826ab..a616ad8 100644 --- a/qml/lib/storage.js +++ b/qml/lib/storage.js @@ -31,52 +31,63 @@ // Get DB Connection function getDB() { - return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000); + return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000); } // Initialize Table if not exists function initialize() { - var db = getDB(); + var db = getDB(); - db.transaction( - function(tx) { - tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);"); - } - ) + db.transaction( + function(tx) { + tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);"); + } + ) } // Get all OTPs into the list model function getOTP() { - var db = getDB(); + var db = getDB(); - db.transaction( - function(tx) { - var res = tx.executeSql("select * from OTPStorage;"); - for (var i=0; i < res.rows.length; i++) { - mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret); - } - } - ) + db.transaction( + function(tx) { + var res = tx.executeSql("select * from OTPStorage;"); + for (var i=0; i < res.rows.length; i++) { + mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret); + } + } + ) } // Add a new OTP function addOTP(title, secret) { - var db = getDB(); + var db = getDB(); - db.transaction( - function(tx) { - tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]); - } - ) + db.transaction( + function(tx) { + tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]); + } + ) } // Remove an existing OTP function removeOTP(title, secret) { - var db = getDB(); + var db = getDB(); - db.transaction( - function(tx) { - tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]); - } - ) + db.transaction( + function(tx) { + tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]); + } + ) +} + +// Change an existing OTP +function changeOTP(title, secret, oldtitle, oldsecret) { + var db = getDB(); + + db.transaction( + function(tx) { + tx.executeSql("UPDATE OTPStorage SET title=?, secret=? WHERE title=? and secret=?;", [title, secret, oldtitle, oldsecret]); + } + ) } diff --git a/qml/pages/About.qml b/qml/pages/About.qml index e75d2f1..e7e7d8f 100644 --- a/qml/pages/About.qml +++ b/qml/pages/About.qml @@ -30,56 +30,54 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +// Define the Layout of the About Page Page { - id: aboutPage - - Image { - id: logo - source: "../sailotp.png" - anchors.horizontalCenter: parent.horizontalCenter - y: 200 + id: aboutPage + Image { + id: logo + source: "../sailotp.png" + anchors.horizontalCenter: parent.horizontalCenter + y: 200 + } + Label { + id: name + anchors.horizontalCenter: parent.horizontalCenter + y: 320 + font.bold: true + text: "SailOTP 0.2" + } + Text { + id: desc + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: name.bottom + anchors.topMargin: 20 + text: "A Simple Sailfish TOTP Generator
(RFC 6238 compatible)" + color: "white" + } + Text { + id: copyright + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: desc.bottom + anchors.topMargin: 20 + text: "Copyright: Stefan Brand
License: BSD (3-clause)" + color: "white" + } + Button { + id: homepage + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: copyright.bottom + anchors.topMargin: 20 + text: "SailOTP on Github" + onClicked: { + Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp") } - - Label { - id: name - anchors.horizontalCenter: parent.horizontalCenter - y: 320 - font.bold: true - text: "SailOTP 0.1" - } - Text { - id: desc - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: name.bottom - anchors.topMargin: 20 - text: "A Simple Sailfish TOTP Generator
(RFC 6238 compatible)" - color: "white" - } - Text { - id: copyright - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: desc.bottom - anchors.topMargin: 20 - text: "Copyright: Stefan Brand
License: BSD (3-clause)" - color: "white" - } - Button { - id: homepage - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: copyright.bottom - anchors.topMargin: 20 - text: "SailOTP on Github" - onClicked: { - Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp") - } - } - Text { - id: accnowledgement - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: homepage.bottom - anchors.topMargin: 20 - text: "SailOTP uses the SHA-1 Implementation
from http://caligatio.github.io/jsSHA/" - color: "white" - } - + } + Text { + id: accnowledgement + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: homepage.bottom + anchors.topMargin: 20 + text: "SailOTP uses the SHA-1 Implementation
from http://caligatio.github.io/jsSHA/" + color: "white" + } } diff --git a/qml/pages/AddOTP.qml b/qml/pages/AddOTP.qml index 3968b96..c68afae 100644 --- a/qml/pages/AddOTP.qml +++ b/qml/pages/AddOTP.qml @@ -30,52 +30,69 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 -import "../lib/storage.js" as DB +import "../lib/storage.js" as DB // Import the storage library for Config-Access +// Define Layout of the Add OTP Dialog Dialog { - id: addOTP + id: addOTP - property QtObject parentPage: null + // We get the Object of the parent page on call to refresh it after adding a new Entry + property QtObject parentPage: null - SilicaFlickable { - id: addOtpList - anchors.fill: parent + // If we want to edit a Key we get title and key from the calling page + property string paramLabel: "" + property string paramKey: "" - VerticalScrollDecorator {} + SilicaFlickable { + id: addOtpList + anchors.fill: parent - Column { - anchors.fill: parent - DialogHeader { - acceptText: "Add" - } + VerticalScrollDecorator {} - TextField { - id: otpLabel - width: parent.width - label: "Title" - placeholderText: "Title for the OTP" - focus: true - horizontalAlignment: TextInput.AlignLeft - } - - TextField { - id: otpSecret - width: parent.width - label: "Secret" - placeholderText: "Secret OTP Key" - focus: true - horizontalAlignment: TextInput.AlignLeft - } - } + Column { + anchors.fill: parent + DialogHeader { + acceptText: paramLabel != "" ? "Save" : "Add" + } + TextField { + id: otpLabel + width: parent.width + label: "Title" + placeholderText: "Title for the OTP" + text: paramLabel != "" ? paramLabel : "" + focus: true + horizontalAlignment: TextInput.AlignLeft + } + TextField { + id: otpSecret + width: parent.width + label: "Secret (at least 16 characters)" + text: paramKey != "" ? paramKey : "" + placeholderText: "Secret OTP Key" + focus: true + horizontalAlignment: TextInput.AlignLeft + } } + } - onDone: { - if (otpLabel.text != "" && otpSecret.text != "") { - DB.addOTP(otpLabel.text, otpSecret.text); - parentPage.refreshOTPList(); - } + // Check if we can Save + canAccept: otpLabel.text.length > 0 && otpSecret.text.length >= 16 ? true : false + + // Save if page is Left with Add + onDone: { + if (result == DialogResult.Accepted) { + // Save the entry to the Config DB + if (paramLabel != "" && paramKey != "") { + // Parameters where filled -> Change existing entry + DB.changeOTP(otpLabel.text, otpSecret.text, paramLabel, paramKey) + } else { + // There were no parameters -> Add new entry + DB.addOTP(otpLabel.text, otpSecret.text); + } + // Refresh the main Page + parentPage.refreshOTPList(); } - + } } diff --git a/qml/pages/MainView.qml b/qml/pages/MainView.qml index 9f7d3de..e5aadb2 100644 --- a/qml/pages/MainView.qml +++ b/qml/pages/MainView.qml @@ -34,139 +34,153 @@ import "../lib/storage.js" as DB import "../lib/crypto.js" as OTP Page { - id: mainPage + id: mainPage - ListModel { - id: otpListModel + ListModel { + id: otpListModel + } + + // This holds the time of the last update of the page as Unix Timestamp (in Milliseconds) + property double lastUpdated: null + + // Add an entry to the list + function appendOTP(title, secret) { + otpListModel.append({"secret": secret, "title": title, "otp": ""}); + } + + // Reload the List of OTPs from storage + function refreshOTPList() { + otpListModel.clear(); + DB.getOTP(); + refreshOTPValues(); + } + + // Calculate new OTPs for every entry + function refreshOTPValues() { + // get seconds from current Date + var curDate = new Date(); + var seconds = curDate.getSeconds(); + + // Iterate over all List entries + for (var i=0; i 2000)) { + var curOTP = OTP.calcOTP(otpListModel.get(i).secret) + otpListModel.setProperty(i, "otp", curOTP); + console.log("Updating Value ", i); + } } - property double lastUpdated: null + // Update the Progressbar + updateProgress.value = 29 - (seconds % 30) + // Set lastUpdate property + lastUpdated = curDate.getTime(); + } - // Add an entry to the list - function appendOTP(title, secret) { - otpListModel.append({"secret": secret, "title": title, "otp": ""}); + Timer { + interval: 1000 + running: Qt.application.active // Timer only runs when App is active + repeat: true + onTriggered: refreshOTPValues(); + } + + SilicaFlickable { + anchors.fill: parent + + PullDownMenu { + MenuItem { + text: "About" + onClicked: pageStack.push(Qt.resolvedUrl("About.qml")) + } + MenuItem { + text: "Add OTP" + onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage}) + } } - // Reload the List of OTPs from storage - function refreshOTPList() { - otpListModel.clear(); - DB.getOTP(); - refreshOTPValues(); + ProgressBar { + id: updateProgress + width: parent.width + maximumValue: 29 + anchors.top: parent.top + anchors.topMargin: 48 } - // Calculate new OTPs for every entry - function refreshOTPValues() { - var curDate = new Date(); - var seconds = curDate.getSeconds(); + SilicaListView { + id: otpList + header: PageHeader { + title: "SailOTP" + } + anchors.fill: parent + model: otpListModel + width: parent.width - for (var i=0; i 2000)) { - var curOTP = OTP.calcOTP(otpListModel.get(i).secret) - otpListModel.setProperty(i, "otp", curOTP); - console.log("Updating Value ", i); - } + ViewPlaceholder { + enabled: otpList.count == 0 + text: "Nothing here" + hintText: "Pull down to add a OTP" + } + + + + delegate: ListItem { + id: otpListItem + menu: otpContextMenu + width: otpList.width + contentHeight: Theme.itemSizeMedium + + function remove() { + // Show 5s countdown, then delete from DB and List + remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) }) } - updateProgress.value = 29 - (seconds % 30) - lastUpdated = curDate.getTime(); - } + ListView.onRemove: animateRemoval() + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter - Timer { - interval: 1000 - running: Qt.application.active - repeat: true + Label { + id: otpLabel + text: model.title + color: Theme.secondaryColor + anchors.horizontalCenter: parent.horizontalCenter + } - onTriggered: refreshOTPValues(); - } + Label { + id: otpValue + anchors.top: otpLabel.bottom + text: model.otp + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + } + } - SilicaFlickable { - anchors.fill: parent - - PullDownMenu { + Component { + id: otpContextMenu + ContextMenu { MenuItem { - text: "About" - onClicked: pageStack.push(Qt.resolvedUrl("About.qml")) + text: "Edit" + onClicked: { + pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret}) + } } MenuItem { - text: "Add OTP" - onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage}) + text: "Delete" + onClicked: remove() } + } } + } + VerticalScrollDecorator{} - ProgressBar { - id: updateProgress - width: parent.width - maximumValue: 29 - anchors.top: parent.top - anchors.topMargin: 48 - } - - SilicaListView { - id: otpList - header: PageHeader { - title: "SailOTP" - } - anchors.fill: parent - model: otpListModel - width: parent.width - - ViewPlaceholder { - enabled: otpList.count == 0 - text: "Nothing here" - hintText: "Pull down to add a OTP" - } - - - - delegate: ListItem { - id: otpListItem - menu: otpContextMenu - width: otpList.width - contentHeight: Theme.itemSizeMedium - - function remove() { - remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) }) - } - - ListView.onRemove: animateRemoval() - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - - Label { - id: otpLabel - text: model.title - color: Theme.secondaryColor - anchors.horizontalCenter: parent.horizontalCenter - } - - Label { - id: otpValue - anchors.top: otpLabel.bottom - text: model.otp - anchors.horizontalCenter: parent.horizontalCenter - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeLarge - } - } - - Component { - id: otpContextMenu - ContextMenu { - MenuItem { - text: "Delete" - onClicked: remove() - } - } - } - } - VerticalScrollDecorator{} - - Component.onCompleted: { - DB.initialize(); - refreshOTPList(); - } - } + Component.onCompleted: { + // Initialize DB (create tables etc..) + DB.initialize(); + // Load list of OTP-Entries + refreshOTPList(); + } } + } } diff --git a/rpm/harbour-sailotp.spec b/rpm/harbour-sailotp.spec index 3e130a4..ed19a0c 100644 --- a/rpm/harbour-sailotp.spec +++ b/rpm/harbour-sailotp.spec @@ -13,7 +13,7 @@ Name: harbour-sailotp %{!?qtc_make:%define qtc_make make} %{?qtc_builddir:%define _builddir %qtc_builddir} Summary: SailOTP -Version: 0.1 +Version: 0.2 Release: 1 Group: Security License: BSD diff --git a/rpm/harbour-sailotp.yaml b/rpm/harbour-sailotp.yaml index b905f0d..2fb7ae8 100644 --- a/rpm/harbour-sailotp.yaml +++ b/rpm/harbour-sailotp.yaml @@ -1,6 +1,6 @@ Name: harbour-sailotp Summary: SailOTP -Version: 0.1 +Version: 0.2 Release: 1 Group: Security URL: https://github.com/seiichiro0185/sailotp/ diff --git a/src/harbour-sailotp.cpp b/src/harbour-sailotp.cpp index 0f897ec..f6d8d32 100644 --- a/src/harbour-sailotp.cpp +++ b/src/harbour-sailotp.cpp @@ -1,32 +1,31 @@ /* - Copyright (C) 2013 Jolla Ltd. - Contact: Thomas Perl - All rights reserved. - - You may use this file under the terms of BSD license as follows: - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the Jolla Ltd nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ + * Copyright (c) 2013, Stefan Brand + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this + * list of conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. The names of the contributors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ #ifdef QT_QML_DEBUG #include @@ -34,9 +33,8 @@ #include - int main(int argc, char *argv[]) { - return SailfishApp::main(argc, argv); + return SailfishApp::main(argc, argv); } From f0b6a630dcabb913c7e20f6480769eeb4c630a10 Mon Sep 17 00:00:00 2001 From: Stefan Brand Date: Fri, 10 Jan 2014 19:57:42 +0100 Subject: [PATCH 2/4] Version 0.3 * Added possibility to "star" an entry to show it on the ActiveCover * Tokens can be copied to the clipboard by tapping on them --- README.md | 13 ++++++-- qml/cover/CoverPage.qml | 52 +++++++++++++++++++++++++---- qml/harbour-sailotp.qml | 7 ++++ qml/lib/storage.js | 62 +++++++++++++++++++++++++++-------- qml/pages/About.qml | 2 +- qml/pages/MainView.qml | 70 +++++++++++++++++++++++++++++++--------- rpm/harbour-sailotp.yaml | 2 +- 7 files changed, 168 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index dddbb0c..b5a4c35 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,16 @@ also known as Timebased One Time Pad (TOPT) as described in RFC 6238. A growing number of sites uses this algorithm for two-factor-authentication, including Github, Linode and several Google services. -At the moment the App is quite basic. One can add new OTP-entries using the -pulley-menu. The main view of the app will show a List off all entries and -their current One-Time-Tokens. The entries will be regenerated every 30 seconds, the remaining time for the current tokens is shown through a progress bar at the top of the app. An entry can be deleted by long-pressing on it. +One can add new OTP-entries using the pulley-menu. The main view of the app will show a list +off all entries and their current One-Time-Tokens. The entries will be regenerated every 30 seconds, +the remaining time for the current tokens is shown through a progress bar at the top of the app. +An entry can be edited or deleted by long-pressing on it. + +One entry can be stared by tapping the star icon on the left. the stared item will be shown +on the ActiveCover and refreshed every 30 seconds. 5 seconds before the token changes it's +color will change to red. The Item can be unstared by tapping it again. + +From the main view a token can be copied to the clipboard by tapping on it. ## Known Limitations diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index e307c99..1388668 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -29,11 +29,39 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 +import "../lib/crypto.js" as OTP // Define the Layout of the Active Cover CoverBackground { + id: coverPage - // Show the SailOTP Logo + property double lastUpdated: 0 + + Timer { + interval: 1000 + // Timer runs only when cover is visible and favourite is set + running: !Qt.application.active && appWin.coverSecret != "" + repeat: true + onTriggered: { + // get seconds from current Date + var curDate = new Date(); + + if (lOTP.text == "" || curDate.getSeconds() == 30 || curDate.getSeconds() == 0 || (curDate.getTime() - lastUpdated > 2000)) { + appWin.coverOTP = OTP.calcOTP(appWin.coverSecret); + } + + // Change color of the OTP to red if less than 5 seconds left + if (29 - (curDate.getSeconds() % 30) < 5) { + lOTP.color = "red" + } else { + lOTP.color = Theme.highlightColor + } + + lastUpdated = curDate.getTime(); + } + } + + // Show the SailOTP Logo Image { id: logo source: "../sailotp.png" @@ -42,10 +70,22 @@ CoverBackground { anchors.topMargin: 48 } - // Show the Application Name - Label { - id: label - anchors.centerIn: parent - text: "SailOTP" + Column { + anchors.top: logo.bottom + anchors.topMargin: 48 + anchors.horizontalCenter: parent.horizontalCenter + + Label { + text: appWin.coverTitle + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.secondaryColor + } + Label { + id: lOTP + text: appWin.coverOTP + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeExtraLarge + } } } diff --git a/qml/harbour-sailotp.qml b/qml/harbour-sailotp.qml index b8632dc..2b9c122 100644 --- a/qml/harbour-sailotp.qml +++ b/qml/harbour-sailotp.qml @@ -33,6 +33,13 @@ import "pages" ApplicationWindow { + id: appWin + + // Properties to pass values between MainPage and Cover + property string coverTitle: "SailOTP" + property string coverSecret: "" + property string coverOTP: "" + initialPage: Component { MainView { } } cover: Qt.resolvedUrl("cover/CoverPage.qml") } diff --git a/qml/lib/storage.js b/qml/lib/storage.js index a616ad8..8dabb15 100644 --- a/qml/lib/storage.js +++ b/qml/lib/storage.js @@ -29,20 +29,34 @@ .import QtQuick.LocalStorage 2.0 as LS -// Get DB Connection +// Get DB Connection, Initialize or Upgrade DB function getDB() { - return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000); -} + try { + var db = LS.LocalStorage.openDatabaseSync("harbour-sailotp", "", "SailOTP Config Storage", 1000000); -// Initialize Table if not exists -function initialize() { - var db = getDB(); - - db.transaction( - function(tx) { - tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);"); + if (db.version == "") { + // Initialize an empty DB, Create the Table + db.changeVersion("", "2", + function(tx) { + tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT, type TEXT, counter INTEGER, fav INTEGER);"); + } + ); + } else if (db.version == "1.0") { + // Upgrade DB Schema to Version 2 + db.changeVersion("1.0", "2", + function(tx) { + tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN type TEXT DEFAULT 'TOTP';"); + tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN counter INTEGER DEFAULT 0;"); + tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN fav INTEGER DEFAULT 0;"); + } + ); } - ) + } catch (e) { + // DB Failed to open + console.log("Could not open DB: " + e); + } + + return db; } // Get all OTPs into the list model @@ -53,7 +67,8 @@ function getOTP() { function(tx) { var res = tx.executeSql("select * from OTPStorage;"); for (var i=0; i < res.rows.length; i++) { - mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret); + mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret, res.rows.item(i).type, res.rows.item(i).counter, res.rows.item(i).fav); + if (res.rows.item(i).fav) mainPage.setCoverOTP(res.rows.item(i).title, res.rows.item(i).secret); } } ) @@ -65,7 +80,7 @@ function addOTP(title, secret) { db.transaction( function(tx) { - tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]); + tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?, ?, ?, ?);", [title, secret, 'TOTP', 0, 0]); } ) } @@ -81,6 +96,27 @@ function removeOTP(title, secret) { ) } +function setFav(title, secret) { + var db = getDB(); + + db.transaction( + function(tx) { + tx.executeSql("UPDATE OTPStorage set fav = 0"); + tx.executeSql("UPDATE OTPStorage set fav = 1 WHERE title=? and secret=?;", [title, secret]); + } + ) +} + +function resetFav(title, secret) { + var db = getDB(); + + db.transaction( + function(tx) { + tx.executeSql("UPDATE OTPStorage set fav = 0"); + } + ) +} + // Change an existing OTP function changeOTP(title, secret, oldtitle, oldsecret) { var db = getDB(); diff --git a/qml/pages/About.qml b/qml/pages/About.qml index e7e7d8f..bd5263f 100644 --- a/qml/pages/About.qml +++ b/qml/pages/About.qml @@ -44,7 +44,7 @@ Page { anchors.horizontalCenter: parent.horizontalCenter y: 320 font.bold: true - text: "SailOTP 0.2" + text: "SailOTP 0.3" } Text { id: desc diff --git a/qml/pages/MainView.qml b/qml/pages/MainView.qml index e5aadb2..a9c7787 100644 --- a/qml/pages/MainView.qml +++ b/qml/pages/MainView.qml @@ -41,18 +41,27 @@ Page { } // This holds the time of the last update of the page as Unix Timestamp (in Milliseconds) - property double lastUpdated: null + property double lastUpdated: 0 // Add an entry to the list - function appendOTP(title, secret) { - otpListModel.append({"secret": secret, "title": title, "otp": ""}); + function appendOTP(title, secret, type, counter, fav) { + otpListModel.append({"secret": secret, "title": title, "fav": fav, "otp": ""}); + } + + // Hand favorite over to the cover + function setCoverOTP(title, secret) { + appWin.coverTitle = title + appWin.coverSecret = secret + if (secret == "") appWin.coverOTP = "" } // Reload the List of OTPs from storage function refreshOTPList() { + otpList.visible = false; otpListModel.clear(); DB.getOTP(); refreshOTPValues(); + otpList.visible = true; } // Calculate new OTPs for every entry @@ -67,7 +76,6 @@ Page { if (otpListModel.get(i).otp == "" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) { var curOTP = OTP.calcOTP(otpListModel.get(i).secret) otpListModel.setProperty(i, "otp", curOTP); - console.log("Updating Value ", i); } } @@ -79,7 +87,8 @@ Page { Timer { interval: 1000 - running: Qt.application.active // Timer only runs when App is active + // Timer only runs when app is acitive and we have entries + running: Qt.application.active && otpListModel.count repeat: true onTriggered: refreshOTPValues(); } @@ -104,6 +113,8 @@ Page { maximumValue: 29 anchors.top: parent.top anchors.topMargin: 48 + // Only show when there are enries + visible: otpListModel.count } SilicaListView { @@ -121,37 +132,66 @@ Page { hintText: "Pull down to add a OTP" } - - delegate: ListItem { id: otpListItem menu: otpContextMenu - width: otpList.width contentHeight: Theme.itemSizeMedium + width: parent.width function remove() { - // Show 5s countdown, then delete from DB and List + // Show 5s countdown, then delete from DB and List remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) }) } + onClicked: { + Clipboard.text = otp + } + ListView.onRemove: animateRemoval() Rectangle { + id: listRow + width: parent.width anchors.horizontalCenter: parent.horizontalCenter - Label { + IconButton { + icon.source: fav == 1 ? "image://theme/icon-m-favorite-selected" : "image://theme/icon-m-favorite" + anchors.left: parent.left + onClicked: { + if (fav == 0) { + DB.setFav(title, secret) + setCoverOTP(title, secret) + for (var i=0; i Date: Sun, 12 Jan 2014 18:43:17 +0100 Subject: [PATCH 3/4] Version 0.4 * Added possibility to use HOTP-tokens --- README.md | 20 +++++++++++------- qml/cover/CoverPage.qml | 17 ++++++++++++--- qml/harbour-sailotp.qml | 3 ++- qml/lib/crypto.js | 23 +++++++++++++------- qml/lib/storage.js | 27 +++++++++++++++++++----- qml/pages/About.qml | 4 ++-- qml/pages/AddOTP.qml | 29 +++++++++++++++++++++++--- qml/pages/MainView.qml | 45 ++++++++++++++++++++++++++++++---------- rpm/harbour-sailotp.yaml | 4 ++-- 9 files changed, 131 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index b5a4c35..43776f8 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ # SailOTP -SailOTP is a Sailfish Implementation of the Google-Authenticator algorithm, -also known as Timebased One Time Pad (TOPT) as described in RFC 6238. A growing +SailOTP is a Sailfish Implementation of the Google-Authenticator algorithms, +also known as TOPT (timer based) and HOTP (counter based) as described in RFC 6238 and 4226. A growing number of sites uses this algorithm for two-factor-authentication, including Github, Linode and several Google services. -One can add new OTP-entries using the pulley-menu. The main view of the app will show a list -off all entries and their current One-Time-Tokens. The entries will be regenerated every 30 seconds, -the remaining time for the current tokens is shown through a progress bar at the top of the app. +One can add new OTP-entries using the pulley-menu. The type of token can be selected. Title and the shared +secret have to be provided. For counter based HOTP-tokens the counter value for the next update of the +Token can be set. The default of 1 is the standard value for new HOTP-tokens and should not be changed. + +The main view of the app will show a list off all entries and their current One-Time-Tokens. +The entries will be regenerated every 30 seconds, the remaining time for the current tokens is shown +through a progress bar at the top of the app. HOTP-type tokens are not updated automatically, instead +a refresh button is shown on the right of the token to calculate the next value and increment the counter An entry can be edited or deleted by long-pressing on it. One entry can be stared by tapping the star icon on the left. the stared item will be shown -on the ActiveCover and refreshed every 30 seconds. 5 seconds before the token changes it's -color will change to red. The Item can be unstared by tapping it again. +on the ActiveCover. If the Token is timer based, it will be refreshed every 30 seconds. 5 seconds before the token changes it's +color will change to red. For counter based tokens a cover action to calculate the next token is shown instead. +The item can be unstared by tapping the star icon again on the main view. From the main view a token can be copied to the clipboard by tapping on it. diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 1388668..7d504ee 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -30,6 +30,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import "../lib/crypto.js" as OTP +import "../lib/storage.js" as DB // Define the Layout of the Active Cover CoverBackground { @@ -40,14 +41,14 @@ CoverBackground { Timer { interval: 1000 // Timer runs only when cover is visible and favourite is set - running: !Qt.application.active && appWin.coverSecret != "" + running: !Qt.application.active && appWin.coverSecret != "" && appWin.coverType == "TOTP" repeat: true onTriggered: { // get seconds from current Date var curDate = new Date(); - if (lOTP.text == "" || curDate.getSeconds() == 30 || curDate.getSeconds() == 0 || (curDate.getTime() - lastUpdated > 2000)) { - appWin.coverOTP = OTP.calcOTP(appWin.coverSecret); + if (lOTP.text == "------" || curDate.getSeconds() == 30 || curDate.getSeconds() == 0 || (curDate.getTime() - lastUpdated > 2000)) { + appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, "TOTP", 0); } // Change color of the OTP to red if less than 5 seconds left @@ -88,4 +89,14 @@ CoverBackground { font.pixelSize: Theme.fontSizeExtraLarge } } + // CoverAction to update a HOTP-Token, only visible for HOTP-Type Tokens + CoverActionList { + enabled: appWin.coverType == "HOTP" ? true : false + CoverAction { + iconSource: "image://theme/icon-m-refresh" + onTriggered: { + appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, "HOTP", DB.getCounter(appWin.coverTitle, appWin.coverSecret, true)); + } + } + } } diff --git a/qml/harbour-sailotp.qml b/qml/harbour-sailotp.qml index 2b9c122..db90a21 100644 --- a/qml/harbour-sailotp.qml +++ b/qml/harbour-sailotp.qml @@ -38,7 +38,8 @@ ApplicationWindow // Properties to pass values between MainPage and Cover property string coverTitle: "SailOTP" property string coverSecret: "" - property string coverOTP: "" + property string coverType: "" + property string coverOTP: "------" initialPage: Component { MainView { } } cover: Qt.resolvedUrl("cover/CoverPage.qml") diff --git a/qml/lib/crypto.js b/qml/lib/crypto.js index d518466..ca867ae 100644 --- a/qml/lib/crypto.js +++ b/qml/lib/crypto.js @@ -66,18 +66,27 @@ function leftpad(str, len, pad) { // *** Main Function *** // // Calculate an OTP-Value from the given secret -// Parameter is the secret key in Base32-notation -function calcOTP(secret) { +// Parameters are: +// secret: The secret key in Base32-Notation +// tpye: either TOTP for timer based or HOTP for counter based calculation +// counter: counter value for HOTP +function calcOTP(secret, type, counter) { // Convert the key to HEX var key = base32tohex(secret); - // Get current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC) - var epoch = Math.round(new Date().getTime() / 1000.0); - // Get last full 30 / 60 Seconds and convert to HEX - var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); + var factor = ""; + + if (type == "TOTP") { + // Get current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC) + var epoch = Math.round(new Date().getTime() / 1000.0); + // Get last full 30 / 60 Seconds and convert to HEX + factor = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); + } else { + factor = leftpad(dec2hex(counter), 16, '0'); + } try { // Calculate the SHA-1 HMAC Value from time and key - var hmacObj = new SHA.jsSHA(time, 'HEX'); + var hmacObj = new SHA.jsSHA(factor, 'HEX'); var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX"); // Finally convert the HMAC-Value to the corresponding 6-digit token diff --git a/qml/lib/storage.js b/qml/lib/storage.js index 8dabb15..bda96e4 100644 --- a/qml/lib/storage.js +++ b/qml/lib/storage.js @@ -68,19 +68,19 @@ function getOTP() { var res = tx.executeSql("select * from OTPStorage;"); for (var i=0; i < res.rows.length; i++) { mainPage.appendOTP(res.rows.item(i).title, res.rows.item(i).secret, res.rows.item(i).type, res.rows.item(i).counter, res.rows.item(i).fav); - if (res.rows.item(i).fav) mainPage.setCoverOTP(res.rows.item(i).title, res.rows.item(i).secret); + if (res.rows.item(i).fav) mainPage.setCoverOTP(res.rows.item(i).title, res.rows.item(i).secret, res.rows.item(i).type); } } ) } // Add a new OTP -function addOTP(title, secret) { +function addOTP(title, secret, type, counter) { var db = getDB(); db.transaction( function(tx) { - tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?, ?, ?, ?);", [title, secret, 'TOTP', 0, 0]); + tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?, ?, ?, ?);", [title, secret, type, counter, 0]); } ) } @@ -118,12 +118,29 @@ function resetFav(title, secret) { } // Change an existing OTP -function changeOTP(title, secret, oldtitle, oldsecret) { +function changeOTP(title, secret, type, counter, oldtitle, oldsecret) { var db = getDB(); db.transaction( function(tx) { - tx.executeSql("UPDATE OTPStorage SET title=?, secret=? WHERE title=? and secret=?;", [title, secret, oldtitle, oldsecret]); + tx.executeSql("UPDATE OTPStorage SET title=?, secret=?, type=?, counter=? WHERE title=? and secret=?;", [title, secret, type, counter, oldtitle, oldsecret]); } ) } + + +// Get the counter for a HOTP value, incerment the counter on request +function getCounter(title, secret, increment) { + var db = getDB(); + + var res = ""; + + db.transaction( + function(tx) { + res = tx.executeSql("SELECT counter FROM OTPStorage where title=? and secret=?;", [title, secret]); + if (increment) tx.executeSql("UPDATE OTPStorage set counter=counter+1 WHERE title=? and secret=?;", [title, secret]); + } + ) + + return res.rows.item(0).counter; +} diff --git a/qml/pages/About.qml b/qml/pages/About.qml index bd5263f..d2dcf17 100644 --- a/qml/pages/About.qml +++ b/qml/pages/About.qml @@ -44,14 +44,14 @@ Page { anchors.horizontalCenter: parent.horizontalCenter y: 320 font.bold: true - text: "SailOTP 0.3" + text: "SailOTP 0.4" } Text { id: desc anchors.horizontalCenter: parent.horizontalCenter anchors.top: name.bottom anchors.topMargin: 20 - text: "A Simple Sailfish TOTP Generator
(RFC 6238 compatible)" + text: "A Simple Sailfish OTP Generator
(RFC 6238/4226 compatible)" color: "white" } Text { diff --git a/qml/pages/AddOTP.qml b/qml/pages/AddOTP.qml index c68afae..a96b3b1 100644 --- a/qml/pages/AddOTP.qml +++ b/qml/pages/AddOTP.qml @@ -40,8 +40,10 @@ Dialog { property QtObject parentPage: null // If we want to edit a Key we get title and key from the calling page + property string paramType: "TOTP" property string paramLabel: "" property string paramKey: "" + property int paramCounter: 1 // New Counters start at 1 SilicaFlickable { id: addOtpList @@ -54,6 +56,15 @@ Dialog { DialogHeader { acceptText: paramLabel != "" ? "Save" : "Add" } + + ComboBox { + id: typeSel + label: "Type" + menu: ContextMenu { + MenuItem { text: "Time-based (TOTP)"; onClicked: { paramType = "TOTP" } } + MenuItem { text: "Counter-based (HOTP)"; onClicked: { paramType = "HOTP" } } + } + } TextField { id: otpLabel width: parent.width @@ -72,11 +83,23 @@ Dialog { focus: true horizontalAlignment: TextInput.AlignLeft } + TextField { + id: otpCounter + width: parent.width + visible: paramType == "HOTP" ? true : false + label: "Next Counter Value" + text: paramCounter + placeholderText: "Next Value of the Counter" + focus: true + horizontalAlignment: TextInput.AlignLeft + validator: IntValidator { bottom: 0 } + } + Component.onCompleted: { typeSel.currentIndex = paramType == "HOTP" ? 1 : 0 } } } // Check if we can Save - canAccept: otpLabel.text.length > 0 && otpSecret.text.length >= 16 ? true : false + canAccept: otpLabel.text.length > 0 && otpSecret.text.length >= 16 && (paramType == "TOTP" || otpCounter.text.length > 0) ? true : false // Save if page is Left with Add onDone: { @@ -84,10 +107,10 @@ Dialog { // Save the entry to the Config DB if (paramLabel != "" && paramKey != "") { // Parameters where filled -> Change existing entry - DB.changeOTP(otpLabel.text, otpSecret.text, paramLabel, paramKey) + DB.changeOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text, paramLabel, paramKey) } else { // There were no parameters -> Add new entry - DB.addOTP(otpLabel.text, otpSecret.text); + DB.addOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text); } // Refresh the main Page parentPage.refreshOTPList(); diff --git a/qml/pages/MainView.qml b/qml/pages/MainView.qml index a9c7787..eb46901 100644 --- a/qml/pages/MainView.qml +++ b/qml/pages/MainView.qml @@ -45,14 +45,19 @@ Page { // Add an entry to the list function appendOTP(title, secret, type, counter, fav) { - otpListModel.append({"secret": secret, "title": title, "fav": fav, "otp": ""}); + otpListModel.append({"secret": secret, "title": title, "fav": fav, "type": type, "counter": counter, "otp": "------"}); } // Hand favorite over to the cover - function setCoverOTP(title, secret) { + function setCoverOTP(title, secret, type) { appWin.coverTitle = title appWin.coverSecret = secret - if (secret == "") appWin.coverOTP = "" + appWin.coverType = type + if (secret = "") { + appWin.coverOTP = ""; + } else if (type == "HOTP") { + appWin.coverOTP = "------"; + } } // Reload the List of OTPs from storage @@ -72,10 +77,15 @@ Page { // Iterate over all List entries for (var i=0; i 2000)) { - var curOTP = OTP.calcOTP(otpListModel.get(i).secret) - otpListModel.setProperty(i, "otp", curOTP); + if (otpListModel.get(i).type == "TOTP") { + // Only update on full 30 / 60 Seconds or if last run of the Functions is more than 2s in the past (e.g. app was in background) + if (otpListModel.get(i).otp == "------" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) { + var curOTP = OTP.calcOTP(otpListModel.get(i).secret, "TOTP") + otpListModel.setProperty(i, "otp", curOTP); + } + } else if (appWin.coverType == "HOTP" && (curDate.getTime() - lastUpdated > 2000) && otpListModel.get(i).fav == 1) { + // If we are coming back from the CoverPage update OTP value if current favourite is HOTP + otpListModel.setProperty(i, "otp", appWin.coverOTP); } } @@ -86,7 +96,7 @@ Page { } Timer { - interval: 1000 + interval: 500 // Timer only runs when app is acitive and we have entries running: Qt.application.active && otpListModel.count repeat: true @@ -159,7 +169,8 @@ Page { onClicked: { if (fav == 0) { DB.setFav(title, secret) - setCoverOTP(title, secret) + setCoverOTP(title, secret, type) + if (type == "HOTP") appWin.coverOTP = otp for (var i=0; i Date: Mon, 13 Jan 2014 00:00:47 +0100 Subject: [PATCH 4/4] Fixed padding for base32tohex return value --- qml/lib/crypto.js | 2 +- rpm/harbour-sailotp.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qml/lib/crypto.js b/qml/lib/crypto.js index ca867ae..9cba4f0 100644 --- a/qml/lib/crypto.js +++ b/qml/lib/crypto.js @@ -52,7 +52,7 @@ function base32tohex(base32) { var chunk = bits.substr(i, 4); hex = hex + parseInt(chunk, 2).toString(16) ; } - return hex; + return hex.length % 2 ? hex + "0" : hex; } // Pad Strings to given length diff --git a/rpm/harbour-sailotp.yaml b/rpm/harbour-sailotp.yaml index a48c866..a529c1c 100644 --- a/rpm/harbour-sailotp.yaml +++ b/rpm/harbour-sailotp.yaml @@ -1,7 +1,7 @@ Name: harbour-sailotp Summary: SailOTP Version: 0.4 -Release: 1 +Release: 2 Group: Security URL: https://github.com/seiichiro0185/sailotp/ License: "BSD\t"