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