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