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

Added Token Length and Time Derivation Options

This commit is contained in:
seiichiro 2016-07-14 13:41:00 +02:00
parent 8291ef6fc9
commit 4e83005525
10 changed files with 105 additions and 50 deletions

View file

@ -42,12 +42,14 @@ CoverBackground {
// 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, appWin.coverType, 0);
var seconds = (curDate.getSeconds() + appWin.coverDiff) % 30
if (lOTP.text == "------" || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, appWin.coverType, appWin.coverLen, appWin.coverDiff, 0);
}
// Change color of the OTP to red if less than 5 seconds left
if (29 - (curDate.getSeconds() % 30) < 5) {
if (29 - seconds < 5) {
lOTP.color = "red"
} else {
lOTP.color = Theme.highlightColor
@ -101,7 +103,7 @@ CoverBackground {
iconSource: appWin.coverType == "HOTP" ? "image://theme/icon-cover-refresh" : "image://theme/icon-cover-previous"
onTriggered: {
if (appWin.coverType == "HOTP") {
appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, "HOTP", DB.getCounter(appWin.coverTitle, appWin.coverSecret, true));
appWin.coverOTP = OTP.calcOTP(appWin.coverSecret, "HOTP", appWin.CoverLen, 0, DB.getCounter(appWin.coverTitle, appWin.coverSecret, true));
} else {
var index = appWin.coverIndex - 1
if (index < 0) index = appWin.listModel.count - 1

View file

@ -42,6 +42,8 @@ ApplicationWindow
property string coverSecret: ""
property string coverType: ""
property string coverOTP: "------"
property int coverLen: 6
property int coverDiff: 0
property int coverIndex: 0
// Global Listmodel for Tokens
@ -51,8 +53,8 @@ ApplicationWindow
NotifyBanner { id: notify }
// Add an entry to the list
function appendOTP(title, secret, type, counter, fav) {
listModel.append({"secret": secret, "title": title, "fav": fav, "type": type, "counter": counter, "otp": "------"});
function appendOTP(title, secret, type, counter, fav, len, diff) {
listModel.append({"secret": secret, "title": title, "fav": fav, "type": type, "counter": counter, "len": len, "diff": diff, "otp": "------"});
}
// Set the OTP shown on the Cover
@ -61,6 +63,8 @@ ApplicationWindow
coverTitle = listModel.get(index).title;
coverSecret = listModel.get(index).secret;
coverType = listModel.get(index).type;
coverLen = listModel.get(index).len;
coverDiff = listModel.get(index).diff;
coverIndex = index;
if (coverType == "TOTP") { coverOTP = "------"; } else { coverOTP = listModel.get(index).otp; }
for (var i=0; i<listModel.count; i++) {

View file

@ -75,14 +75,16 @@ var steamChars = ['2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C',
// 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) {
// length: length of the returned token
// diff: derivation of time between phone and server
function calcOTP(secret, type, len, diff, counter) {
// Convert the key to HEX
var key = base32tohex(secret);
var factor = "";
if (type.substr(0, 4) == "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 current Time in UNIX Timestamp format (Seconds since 01.01.1970 00:00 UTC), and add derivation value
var epoch = Math.round(new Date().getTime() / 1000.0) + diff;
// Get last full 30 / 60 Seconds and convert to HEX
factor = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0');
} else {
@ -108,7 +110,11 @@ function calcOTP(secret, type, counter) {
}
} else {
otp = code + '';
otp = (otp).substr(otp.length - 6, 6);
otp = (otp).substr(otp.length - len, len);
// pad return code with 0 from the left
for (i=0; i++; otp.length < len) {
otp = "0" + otp;
}
}
} catch (e) {
otp = "Invalid Secret!"

View file

@ -38,22 +38,33 @@ function getDB() {
// Initialize an empty DB, Create the Table
db.changeVersion("", "3",
function(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT, type TEXT DEFAULT 'TOPT', counter INTEGER DEFAULT 0, fav INTEGER DEFAULT 0, sort INTEGER DEFAULT 0);");
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT, type TEXT DEFAULT 'TOPT', counter INTEGER DEFAULT 0, fav INTEGER DEFAULT 0, sort INTEGER DEFAULT 0, len INTEGER default 6, diff INTEGER default 0);");
});
} else if (db.version == "1.0") {
// Upgrade DB Schema to Version 3
db.changeVersion("1.0", "3",
// Upgrade DB Schema to Version 4
db.changeVersion("1.0", "4",
function(tx) {
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN type TEXT DEFAULT 'TOTP';");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN counter INTEGER DEFAULT 0;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN fav INTEGER DEFAULT 0;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN sort INTEGER DEFAULT 0;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN len INTEGER DEFAULT 6;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN diff INTEGER DEFAULT 0;");
});
} else if (db.version == "2") {
// Upgrade DB Schema to Version 3
db.changeVersion("2", "3",
db.changeVersion("2", "4",
function(tx) {
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN sort INTEGER DEFAULT 0;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN len INTEGER DEFAULT 6;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN diff INTEGER DEFAULT 0;");
});
} else if (db.version == "3") {
// Upgrade DB Schema to Version 4
db.changeVersion("3", "4",
function(tx) {
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN len INTEGER DEFAULT 6;");
tx.executeSql("ALTER TABLE OTPStorage ADD COLUMN diff INTEGER DEFAULT 0;");
});
}
} catch (e) {
@ -72,7 +83,7 @@ function getOTP() {
function(tx) {
var res = tx.executeSql("select * from OTPStorage order by sort;");
for (var i=0; i < res.rows.length; i++) {
appWin.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, res.rows.item(i).sort);
appWin.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, res.rows.item(i).len, res.rows.item(i).diff);
if (res.rows.item(i).fav) appWin.setCover(i);
}
});
@ -93,12 +104,14 @@ function db2json() {
"type": res.rows.item(i).type,
"counter": res.rows.item(i).counter,
"sort": res.rows.item(i).sort,
"len": res.rows.item(i).len,
"diff": res.rows.item(i).diff,
});
}
});
if (otpList.length > 0) {
return(JSON.stringify({"app": "sailotp", "version": 2, "otplist": otpList}));
return(JSON.stringify({"app": "sailotp", "version": 3, "otplist": otpList}));
} else {
return("")
}
@ -109,7 +122,7 @@ function json2db(jsonString, error) {
var json = JSON.parse(jsonString);
error = "";
if ((json.version != "1" || json.version != "2") && json.app != "sailotp" ) {
if ((json.version != "1" || json.version != "2" || json.version != "3") && json.app != "sailotp" ) {
error = "Unrecognized format, file is not a SailOTP export";
return(false);
} else {
@ -120,9 +133,11 @@ function json2db(jsonString, error) {
var otpItem = otpList.shift();
if (otpItem.title != "" & otpItem.secret.length >= 16) {
if (json.version == "1") {
addOTP(otpItem.title, otpItem.secret, otpItem.type, otpItem.counter, 0);
addOTP(otpItem.title, otpItem.secret, otpItem.type, otpItem.counter, 0, 6, 0);
} else if (json.version == "2") {
addOTP(otpItem.title, otpItem.secret, otpItem.type, otpItem.counter, otpItem.sort, 6, 0);
} else {
addOTP(otpItem.title, otpItem.secret, otpItem.type, otpItem.counter, otpItem.sort);
addOTP(otpItem.title, otpItem.secret, otpItem.type, otpItem.counter, otpItem.sort, otpItem.len, otpItem.diff);
}
}
}
@ -136,7 +151,7 @@ function json2db(jsonString, error) {
}
// Add a new OTP
function addOTP(title, secret, type, counter, sort) {
function addOTP(title, secret, type, counter, sort, len, diff) {
var db = getDB();
db.transaction(
@ -144,7 +159,7 @@ function addOTP(title, secret, type, counter, sort) {
if (checkOTP(title, secret)) {
console.log("Token " + title + " is already in DB");
} else {
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?, ?, ?, ?, ?);", [title, secret, type, counter, 0, sort]);
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?, ?, ?, ?, ?, ?, ?);", [title, secret, type, counter, 0, sort, len, diff]);
console.log("Token " + title + " added.");
}
});
@ -195,12 +210,12 @@ function resetFav(title, secret) {
}
// Change an existing OTP
function changeOTP(title, secret, type, counter, oldtitle, oldsecret) {
function changeOTP(title, secret, type, counter, len, diff, oldtitle, oldsecret) {
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("UPDATE OTPStorage SET title=?, secret=?, type=?, counter=? WHERE title=? and secret=?;", [title, secret, type, counter, oldtitle, oldsecret]);
tx.executeSql("UPDATE OTPStorage SET title=?, secret=?, type=?, counter=?, len=?, diff=? WHERE title=? and secret=?;", [title, secret, type, counter, len, diff, oldtitle, oldsecret]);
console.log("Token " + title + " modified.");
}
);

View file

@ -45,6 +45,8 @@ Dialog {
property string paramType: "TOTP"
property string paramLabel: ""
property string paramKey: ""
property int paramLen: 6
property int paramDiff: 0
property int paramCounter: 1 // New Counters start at 1
property bool paramNew: false
@ -70,7 +72,7 @@ Dialog {
if (((paramType == "TOTP" || paramType == "TOTP_STEAM") && (otpLabel.text == "" || otpSecret.text == "")) || (paramType == "HOTP" && (otpLabel.text == "" || otpSecret.text == "" || otpCounter.text <= 0))) {
notify.show(qsTr("Can't create QR-Code from incomplete settings!"), 4000);
} else {
pageStack.push(Qt.resolvedUrl("QRPage.qml"), {paramLabel: otpLabel.text, paramKey: otpSecret.text, paramType: paramType, paramCounter: otpCounter.text});
pageStack.push(Qt.resolvedUrl("QRPage.qml"), {paramLabel: otpLabel.text, paramKey: otpSecret.text, paramType: paramType, paramCounter: otpCounter.text, paramLen: otpLen.text});
}
}
}
@ -116,8 +118,35 @@ Dialog {
horizontalAlignment: TextInput.AlignLeft
EnterKey.enabled: text.length > 15
EnterKey.iconSource: paramType == "HOTP" ? "image://theme/icon-m-enter-next" : "image://theme/icon-m-enter-accept"
EnterKey.onClicked: paramType == "HOTP" ? otpCounter.focus = true : addOTP.accept()
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: otpLen.focus = true
}
TextField {
id: otpLen
width: parent.width
label: qsTr("Length")
text: paramLen
placeholderText: qsTr("Length of the Token")
focus: true
horizontalAlignment: TextInput.AlignLeft
validator: IntValidator { bottom: 1 }
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: paramType == "HOTP" ? otpCounter.focus = true : otpDiff.focus = true
}
TextField {
id: otpDiff
width: parent.width
visible: paramType == "TOTP" ? true : false
label: qsTr("Time Derivation (Seconds)")
text: paramDiff
placeholderText: qsTr("Time Derivation (Seconds)")
focus: true
horizontalAlignment: TextInput.AlignLeft
validator: IntValidator {}
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: addOTP.accept()
}
TextField {
id: otpCounter
@ -138,7 +167,7 @@ Dialog {
}
// Check if we can Save
canAccept: otpLabel.text.length > 0 && otpSecret.text.length >= 16 && (paramType == "TOTP" || paramType == "TOTP_STEAM" || otpCounter.text.length > 0) ? true : false
canAccept: otpLabel.text.length > 0 && otpSecret.text.length >= 16 && otpLen.text >= 1 && ((paramType == "TOTP" && otpDiff.text != "") || paramType == "TOTP_STEAM" || otpCounter.text.length > 0) ? true : false
// Save if page is Left with Add
onDone: {
@ -146,10 +175,10 @@ Dialog {
// Save the entry to the Config DB
if (paramLabel != "" && paramKey != "" && !paramNew) {
// Parameters where filled -> Change existing entry
DB.changeOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text, paramLabel, paramKey)
DB.changeOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text, otpLen.text, otpDiff.text, paramLabel, paramKey)
} else {
// There were no parameters -> Add new entry
DB.addOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text, appWin.listModel.count);
DB.addOTP(otpLabel.text, otpSecret.text, paramType, otpCounter.text, appWin.listModel.count, otpLen.text, otpDiff.text);
}
// Refresh the main Page

View file

@ -41,18 +41,6 @@ Page {
// This holds the time of the last update of the page as Unix Timestamp (in Milliseconds)
property double lastUpdated: 0
// Hand favorite over to the cover
function setCoverOTP(title, secret, type) {
appWin.coverTitle = title
appWin.coverSecret = secret
appWin.coverType = type
if (secret == "") {
appWin.coverOTP = "";
} else if (type == "HOTP") {
appWin.coverOTP = "------";
}
}
// Reload the List of OTPs from storage
function refreshOTPList() {
otpList.visible = false;
@ -68,14 +56,16 @@ Page {
function refreshOTPValues() {
// get seconds from current Date
var curDate = new Date();
var seconds = curDate.getSeconds();
var seconds_global = curDate.getSeconds() % 30
// Iterate over all List entries
for (var i=0; i<appWin.listModel.count; i++) {
if (appWin.listModel.get(i).type == "TOTP" || appWin.listModel.get(i).type == "TOTP_STEAM" ) {
// Take derivation into account if set
var seconds = (curDate.getSeconds() + appWin.listModel.get(i).diff) % 30;
// 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 (appWin.listModel.get(i).otp == "------" || seconds == 30 || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
var curOTP = OTP.calcOTP(appWin.listModel.get(i).secret, appWin.listModel.get(i).type)
if (appWin.listModel.get(i).otp == "------" || seconds == 0 || (curDate.getTime() - lastUpdated > 2000)) {
var curOTP = OTP.calcOTP(appWin.listModel.get(i).secret, appWin.listModel.get(i).type, appWin.listModel.get(i).len, appWin.listModel.get(i).diff, 0)
appWin.listModel.setProperty(i, "otp", curOTP);
}
} else if (appWin.coverType == "HOTP" && (curDate.getTime() - lastUpdated > 2000) && appWin.listModel.get(i).fav == 1) {
@ -85,7 +75,7 @@ Page {
}
// Update the Progressbar
updateProgress.value = 29 - (seconds % 30)
updateProgress.value = 29 - seconds_global
// Set lastUpdate property
lastUpdated = curDate.getTime();
}
@ -228,7 +218,7 @@ Page {
visible: type == "HOTP" ? true : false
onClicked: {
appWin.listModel.setProperty(index, "counter", DB.getCounter(title, secret, true));
appWin.listModel.setProperty(index, "otp", OTP.calcOTP(secret, "HOTP", counter));
appWin.listModel.setProperty(index, "otp", OTP.calcOTP(secret, "HOTP", len, 0, counter));
if (fav == 1) appWin.coverOTP = otp;
}
}
@ -249,7 +239,7 @@ Page {
MenuItem {
text: qsTr("Edit")
onClicked: {
pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret, paramType: type, paramCounter: DB.getCounter(title, secret, false)})
pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage, paramLabel: title, paramKey: secret, paramType: type, paramLen: len, paramDiff: diff, paramCounter: DB.getCounter(title, secret, false)})
}
}
MenuItem {

View file

@ -38,6 +38,7 @@ Page {
property string paramType: ""
property string paramLabel: ""
property string paramKey: ""
property int paramLen: 6
property int paramCounter: 0
Label {
@ -65,7 +66,7 @@ Page {
} else if (paramType == "HOTP") {
if (paramLabel != "" && paramKey != "" && paramCounter > 0)
otpurl = "otpauth://hotp/"+paramLabel+"?secret="+paramKey+"&counter="+paramCounter;
otpurl = "otpauth://hotp/"+paramLabel+"?secret="+paramKey+"&counter="+paramCounter+"&digits="+paramLen;
}
if (otpurl != "") {
qrImage.source = "image://qqrencoder/"+otpurl;

View file

@ -90,9 +90,13 @@ Page {
onTagFound: {
var ret = URL.decode(tag);
var len = 6
scanning = false
if (ret && ret.type != "" && ret.title != "" && ret.secret != "" && (ret.counter != "" || ret.type == "TOTP")) {
pageStack.replace(Qt.resolvedUrl("AddOTP.qml"), {parentPage: parentPage, paramLabel: ret.title, paramKey: ret.secret, paramType: ret.type, paramCounter: ret.counter, paramNew: true})
if (ret.digits != "") {
len = ret.digits
}
pageStack.replace(Qt.resolvedUrl("AddOTP.qml"), {parentPage: parentPage, paramLabel: ret.title, paramKey: ret.secret, paramType: ret.type, paramCounter: ret.counter, paramLen: len, paramNew: true})
} else {
notify.show(qsTr("No valid Token data found."), 3000);
}

View file

@ -1,3 +1,7 @@
* Thu Jul 14 2016 Stefan Brand <sailfish@seiichiro0185.org> 1.4-1
- Added Setting for Time Derivation
- Added Setting for Token Length
* Sun Dec 06 2015 Stefan Brand <sailfish@seiichiro0185.org> 1.3-1
- Added SteamGuard OTP Type (Thanks to Robin Appelman)

View file

@ -1,6 +1,6 @@
Name: harbour-sailotp
Summary: SailOTP
Version: 1.3
Version: 1.4
Release: 1
Group: Security
URL: https://github.com/seiichiro0185/sailotp/