mirror of
https://github.com/seiichiro0185/sailotp.git
synced 2024-11-21 23:39:41 +00:00
Version 0.4
* Added possibility to use HOTP-tokens
This commit is contained in:
parent
f0b6a630dc
commit
430607231c
9 changed files with 131 additions and 41 deletions
20
README.md
20
README.md
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue