1
0
Fork 0
mirror of https://github.com/seiichiro0185/sailotp.git synced 2024-11-25 08:39:42 +00:00

Added HOTP support

This commit is contained in:
seiichiro 2014-01-12 18:03:23 +01:00
parent 5ddc4dcf90
commit f86a8c74a3
6 changed files with 99 additions and 38 deletions

View file

@ -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));
}
}
}
}

View file

@ -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")

View file

@ -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

View file

@ -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;
}

View file

@ -43,27 +43,26 @@ Dialog {
property string paramType: "TOTP"
property string paramLabel: ""
property string paramKey: ""
property int paramCounter: 0
property int paramCounter: 1 // New Counters start at 1
SilicaFlickable {
id: addOtpList
anchors.fill: parent
width: parent.width
VerticalScrollDecorator {}
Column {
anchors.fill: parent
width: parent.width
DialogHeader {
acceptText: paramLabel != "" ? "Save" : "Add"
}
ComboBox {
id: typeSel
label: "Type"
menu: ContextMenu {
MenuItem { text: "Time-based"; onClicked: { paramType = "TOTP" } }
MenuItem { text: "Counter-based"; onClicked: { paramType = "HOTP" } }
MenuItem { text: "Time-based (TOTP)"; onClicked: { paramType = "TOTP" } }
MenuItem { text: "Counter-based (HOTP)"; onClicked: { paramType = "HOTP" } }
}
}
TextField {
@ -88,18 +87,19 @@ Dialog {
id: otpCounter
width: parent.width
visible: paramType == "HOTP" ? true : false
label: "Counter Value"
label: "Next Counter Value"
text: paramCounter
placeholderText: "Initial Value of the Counter"
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: {
@ -107,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();

View file

@ -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<otpListModel.count; i++) {
// 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)
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<otpListModel.count; i++) {
if (i != index) {
otpListModel.setProperty(i, "fav", 0);
@ -169,7 +180,7 @@ Page {
}
} else {
DB.resetFav(title, secret)
setCoverOTP("SailOTP", "")
setCoverOTP("SailOTP", "", "")
otpListModel.setProperty(index, "fav", 0);
}
}
@ -195,13 +206,25 @@ Page {
}
}
// Show an update button on HTOP-Type Tokens
IconButton {
icon.source: "image://theme/icon-m-refresh"
anchors.right: parent.right
visible: type == "HOTP" ? true : false
onClicked: {
otpListModel.setProperty(index, "counter", DB.getCounter(title, secret, true));
otpListModel.setProperty(index, "otp", OTP.calcOTP(secret, "HOTP", counter));
if (fav == 1) appWin.coverOTP = otp;
}
}
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: "Edit"
onClicked: {
pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret})
pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret, paramType: type, paramCounter: DB.getCounter(title, secret, false)})
}
}
MenuItem {