From f0b6a630dcabb913c7e20f6480769eeb4c630a10 Mon Sep 17 00:00:00 2001 From: Stefan Brand Date: Fri, 10 Jan 2014 19:57:42 +0100 Subject: [PATCH] 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