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

Version 0.4

* Added possibility to use HOTP-tokens
This commit is contained in:
seiichiro 2014-01-12 18:43:17 +01:00
parent f0b6a630dc
commit 430607231c
9 changed files with 131 additions and 41 deletions

View file

@ -1,18 +1,24 @@
# SailOTP # SailOTP
SailOTP is a Sailfish Implementation of the Google-Authenticator algorithm, SailOTP is a Sailfish Implementation of the Google-Authenticator algorithms,
also known as Timebased One Time Pad (TOPT) as described in RFC 6238. A growing 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 number of sites uses this algorithm for two-factor-authentication, including
Github, Linode and several Google services. 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 One can add new OTP-entries using the pulley-menu. The type of token can be selected. Title and the shared
off all entries and their current One-Time-Tokens. The entries will be regenerated every 30 seconds, secret have to be provided. For counter based HOTP-tokens the counter value for the next update of the
the remaining time for the current tokens is shown through a progress bar at the top of the app. 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. 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 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 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. The Item can be unstared by tapping it again. 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. From the main view a token can be copied to the clipboard by tapping on it.

View file

@ -30,6 +30,7 @@
import QtQuick 2.0 import QtQuick 2.0
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import "../lib/crypto.js" as OTP import "../lib/crypto.js" as OTP
import "../lib/storage.js" as DB
// Define the Layout of the Active Cover // Define the Layout of the Active Cover
CoverBackground { CoverBackground {
@ -40,14 +41,14 @@ CoverBackground {
Timer { Timer {
interval: 1000 interval: 1000
// Timer runs only when cover is visible and favourite is set // 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 repeat: true
onTriggered: { onTriggered: {
// get seconds from current Date // get seconds from current Date
var curDate = new Date(); var curDate = new Date();
if (lOTP.text == "" || curDate.getSeconds() == 30 || curDate.getSeconds() == 0 || (curDate.getTime() - lastUpdated > 2000)) { if (lOTP.text == "------" || curDate.getSeconds() == 30 || curDate.getSeconds() == 0 || (curDate.getTime() - lastUpdated > 2000)) {
appWin.coverOTP = OTP.calcOTP(appWin.coverSecret); appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, "TOTP", 0);
} }
// Change color of the OTP to red if less than 5 seconds left // Change color of the OTP to red if less than 5 seconds left
@ -88,4 +89,14 @@ CoverBackground {
font.pixelSize: Theme.fontSizeExtraLarge 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 // Properties to pass values between MainPage and Cover
property string coverTitle: "SailOTP" property string coverTitle: "SailOTP"
property string coverSecret: "" property string coverSecret: ""
property string coverOTP: "" property string coverType: ""
property string coverOTP: "------"
initialPage: Component { MainView { } } initialPage: Component { MainView { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml") cover: Qt.resolvedUrl("cover/CoverPage.qml")

View file

@ -66,18 +66,27 @@ function leftpad(str, len, pad) {
// *** Main Function *** // // *** Main Function *** //
// Calculate an OTP-Value from the given secret // Calculate an OTP-Value from the given secret
// Parameter is the secret key in Base32-notation // Parameters are:
function calcOTP(secret) { // 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 // Convert the key to HEX
var key = base32tohex(secret); var key = base32tohex(secret);
// Get current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC) var factor = "";
var epoch = Math.round(new Date().getTime() / 1000.0);
// Get last full 30 / 60 Seconds and convert to HEX if (type == "TOTP") {
var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0'); // 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 { try {
// Calculate the SHA-1 HMAC Value from time and key // 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"); var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX");
// Finally convert the HMAC-Value to the corresponding 6-digit token // 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;"); var res = tx.executeSql("select * from OTPStorage;");
for (var i=0; i < res.rows.length; i++) { 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); 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 // Add a new OTP
function addOTP(title, secret) { function addOTP(title, secret, type, counter) {
var db = getDB(); var db = getDB();
db.transaction( db.transaction(
function(tx) { 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 // Change an existing OTP
function changeOTP(title, secret, oldtitle, oldsecret) { function changeOTP(title, secret, type, counter, oldtitle, oldsecret) {
var db = getDB(); var db = getDB();
db.transaction( db.transaction(
function(tx) { 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

@ -44,14 +44,14 @@ Page {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
y: 320 y: 320
font.bold: true font.bold: true
text: "SailOTP 0.3" text: "SailOTP 0.4"
} }
Text { Text {
id: desc id: desc
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: name.bottom anchors.top: name.bottom
anchors.topMargin: 20 anchors.topMargin: 20
text: "A Simple Sailfish TOTP Generator<br />(RFC 6238 compatible)" text: "A Simple Sailfish OTP Generator<br />(RFC 6238/4226 compatible)"
color: "white" color: "white"
} }
Text { Text {

View file

@ -40,8 +40,10 @@ Dialog {
property QtObject parentPage: null property QtObject parentPage: null
// If we want to edit a Key we get title and key from the calling page // 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 paramLabel: ""
property string paramKey: "" property string paramKey: ""
property int paramCounter: 1 // New Counters start at 1
SilicaFlickable { SilicaFlickable {
id: addOtpList id: addOtpList
@ -54,6 +56,15 @@ Dialog {
DialogHeader { DialogHeader {
acceptText: paramLabel != "" ? "Save" : "Add" 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 { TextField {
id: otpLabel id: otpLabel
width: parent.width width: parent.width
@ -72,11 +83,23 @@ Dialog {
focus: true focus: true
horizontalAlignment: TextInput.AlignLeft 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 // 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 // Save if page is Left with Add
onDone: { onDone: {
@ -84,10 +107,10 @@ Dialog {
// Save the entry to the Config DB // Save the entry to the Config DB
if (paramLabel != "" && paramKey != "") { if (paramLabel != "" && paramKey != "") {
// Parameters where filled -> Change existing entry // 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 { } else {
// There were no parameters -> Add new entry // 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 // Refresh the main Page
parentPage.refreshOTPList(); parentPage.refreshOTPList();

View file

@ -45,14 +45,19 @@ Page {
// Add an entry to the list // Add an entry to the list
function appendOTP(title, secret, type, counter, fav) { 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 // Hand favorite over to the cover
function setCoverOTP(title, secret) { function setCoverOTP(title, secret, type) {
appWin.coverTitle = title appWin.coverTitle = title
appWin.coverSecret = secret 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 // Reload the List of OTPs from storage
@ -72,10 +77,15 @@ Page {
// Iterate over all List entries // Iterate over all List entries
for (var i=0; i<otpListModel.count; i++) { 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).type == "TOTP") {
if (otpListModel.get(i).otp == "" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) { // 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)
var curOTP = OTP.calcOTP(otpListModel.get(i).secret) if (otpListModel.get(i).otp == "------" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
otpListModel.setProperty(i, "otp", curOTP); 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 { Timer {
interval: 1000 interval: 500
// Timer only runs when app is acitive and we have entries // Timer only runs when app is acitive and we have entries
running: Qt.application.active && otpListModel.count running: Qt.application.active && otpListModel.count
repeat: true repeat: true
@ -159,7 +169,8 @@ Page {
onClicked: { onClicked: {
if (fav == 0) { if (fav == 0) {
DB.setFav(title, secret) 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++) { for (var i=0; i<otpListModel.count; i++) {
if (i != index) { if (i != index) {
otpListModel.setProperty(i, "fav", 0); otpListModel.setProperty(i, "fav", 0);
@ -169,7 +180,7 @@ Page {
} }
} else { } else {
DB.resetFav(title, secret) DB.resetFav(title, secret)
setCoverOTP("SailOTP", "") setCoverOTP("SailOTP", "", "")
otpListModel.setProperty(index, "fav", 0); 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 { Component {
id: otpContextMenu id: otpContextMenu
ContextMenu { ContextMenu {
MenuItem { MenuItem {
text: "Edit" text: "Edit"
onClicked: { 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 { MenuItem {

View file

@ -1,6 +1,6 @@
Name: harbour-sailotp Name: harbour-sailotp
Summary: SailOTP Summary: SailOTP
Version: 0.3 Version: 0.4
Release: 1 Release: 1
Group: Security Group: Security
URL: https://github.com/seiichiro0185/sailotp/ URL: https://github.com/seiichiro0185/sailotp/
@ -8,7 +8,7 @@ License: "BSD\t"
Sources: Sources:
- '%{name}-%{version}.tar.bz2' - '%{name}-%{version}.tar.bz2'
Description: | Description: |
A Sailfish implementation of the Timebased One Time Pad algorithm as used by Google Authenticator and a growing number of Websites. A Sailfish implementation of the One Time Pad algorithm as used by Google Authenticator and a growing number of Websites.
Configure: none Configure: none
Builder: qtc5 Builder: qtc5
PkgConfigBR: PkgConfigBR: