1
0
Fork 0
mirror of https://github.com/seiichiro0185/sailotp.git synced 2024-05-18 08:30:54 +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 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.

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

@ -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<br />(RFC 6238 compatible)"
text: "A Simple Sailfish OTP Generator<br />(RFC 6238/4226 compatible)"
color: "white"
}
Text {

View file

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

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 {

View file

@ -1,6 +1,6 @@
Name: harbour-sailotp
Summary: SailOTP
Version: 0.3
Version: 0.4
Release: 1
Group: Security
URL: https://github.com/seiichiro0185/sailotp/
@ -8,7 +8,7 @@ License: "BSD\t"
Sources:
- '%{name}-%{version}.tar.bz2'
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
Builder: qtc5
PkgConfigBR: