Cleanup and Comments for publishing

* Added Readme and License
* Cleaned up the code
* Added comments to the code
This commit is contained in:
seiichiro 2014-01-06 20:08:16 +01:00
parent 0c0f10f7ab
commit f98444d5a4
10 changed files with 386 additions and 301 deletions

26
COPYING Normal file
View File

@ -0,0 +1,26 @@
Copyright (c) 2013, Stefan Brand <seiichiro@seiichiro0185.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
3. The names of the contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# 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
number of sites uses this algorithm for two-factor-authentication, including
Github, Linode and several Google services.
At the moment the App is quite basic. 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. An entry can be deleted by long-pressing on it.
## Known Limitations
At the moment the only way to insert new entries into the app is to insert the
title and secret key by hand. It's not possible to use the QR-Codes some sites
provide directly.
## Contact and Issues
If you find any bugs or want to suggest a feature, feel free to use Githubs
Issues feature.
## License
SailOTP is licensed under a 3-Clause BSD-License. See COPYING for details.
## Accnowledgements
SailOTP uses the SHA-1 and HMAC-Implementation from
<a href="https://github.com/Caligatio/jsSHA" target="_blank">https://github.com/Caligatio/jsSHA</a>
The implementation of the TOTP-algorithm was inspired by:
<a href="http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/" target="_blank">http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/</a>

View File

@ -30,19 +30,22 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
// Define the Layout of the Active Cover
CoverBackground {
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 48
}
// Show the SailOTP Logo
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 48
}
Label {
id: label
anchors.centerIn: parent
text: "SailOTP"
}
// Show the Application Name
Label {
id: label
anchors.centerIn: parent
text: "SailOTP"
}
}

View File

@ -33,8 +33,8 @@ import "pages"
ApplicationWindow
{
initialPage: Component { MainView { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
initialPage: Component { MainView { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
}

View File

@ -29,49 +29,62 @@
.import "./sha.js" as SHA
// Helper Functions
// *** Helper Functions *** //
// Decimal to HEX
function dec2hex(s) { return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); }
// HEX to Decimal
function hex2dec(s) { return parseInt(s, 16); }
// Convert Base32-secret to HEX Value
function base32tohex(base32) {
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var bits = "";
var hex = "";
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var bits = "";
var hex = "";
for (var i = 0; i < base32.length; i++) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, '0');
}
for (var i = 0; i+4 <= bits.length; i+=4) {
var chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16) ;
}
return hex;
for (var i = 0; i < base32.length; i++) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += leftpad(val.toString(2), 5, '0');
}
for (var i = 0; i+4 <= bits.length; i+=4) {
var chunk = bits.substr(i, 4);
hex = hex + parseInt(chunk, 2).toString(16) ;
}
return hex;
}
// Pad Strings to given length
function leftpad(str, len, pad) {
if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str;
}
return str;
if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str;
}
return str;
}
// *** Main Function *** //
// Calculate an OTP-Value from the given secret
// Parameter is the secret key in Base32-notation
function calcOTP(secret) {
var key = base32tohex(secret);
var epoch = Math.round(new Date().getTime() / 1000.0);
var time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, '0');
// 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 hmacObj = new SHA.jsSHA(time, 'HEX');
var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX");
// Calculate the SHA-1 HMAC Value from time and key
var hmacObj = new SHA.jsSHA(time, 'HEX');
var hmac = hmacObj.getHMAC(key, 'HEX', 'SHA-1', "HEX");
var offset = hex2dec(hmac.substring(hmac.length - 1));
// Finally convert the HMAC-Value to the corresponding 6-digit token
var offset = hex2dec(hmac.substring(hmac.length - 1));
var otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec('7fffffff')) + '';
otp = (otp).substr(otp.length - 6, 6);
return otp;
var otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec('7fffffff')) + '';
otp = (otp).substr(otp.length - 6, 6);
// return the calculated token
return otp;
}

View File

@ -31,52 +31,52 @@
// Get DB Connection
function getDB() {
return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000);
return LS.LocalStorage.openDatabaseSync("harbour-sailotp", "1.0", "SailOTP Config Storage", 1000000);
}
// Initialize Table if not exists
function initialize() {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);");
}
)
db.transaction(
function(tx) {
tx.executeSql("CREATE TABLE IF NOT EXISTS OTPStorage(title TEXT, secret TEXT);");
}
)
}
// Get all OTPs into the list model
function getOTP() {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
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);
}
}
)
db.transaction(
function(tx) {
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);
}
}
)
}
// Add a new OTP
function addOTP(title, secret) {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]);
}
)
db.transaction(
function(tx) {
tx.executeSql("INSERT INTO OTPStorage VALUES(?, ?);", [title, secret]);
}
)
}
// Remove an existing OTP
function removeOTP(title, secret) {
var db = getDB();
var db = getDB();
db.transaction(
function(tx) {
tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]);
}
)
db.transaction(
function(tx) {
tx.executeSql("DELETE FROM OTPStorage WHERE title=? and secret=?;", [title, secret]);
}
)
}

View File

@ -30,56 +30,54 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
// Define the Layout of the About Page
Page {
id: aboutPage
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
y: 200
id: aboutPage
Image {
id: logo
source: "../sailotp.png"
anchors.horizontalCenter: parent.horizontalCenter
y: 200
}
Label {
id: name
anchors.horizontalCenter: parent.horizontalCenter
y: 320
font.bold: true
text: "SailOTP 0.1"
}
Text {
id: desc
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: name.bottom
anchors.topMargin: 20
text: "A Simple Sailfish TOTP Generator<br />(RFC 6238 compatible)"
color: "white"
}
Text {
id: copyright
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: desc.bottom
anchors.topMargin: 20
text: "Copyright: Stefan Brand<br />License: BSD (3-clause)"
color: "white"
}
Button {
id: homepage
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: copyright.bottom
anchors.topMargin: 20
text: "<a href=\"https://github.com/seiichiro0185/sailotp\">SailOTP on Github</a>"
onClicked: {
Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp")
}
Label {
id: name
anchors.horizontalCenter: parent.horizontalCenter
y: 320
font.bold: true
text: "SailOTP 0.1"
}
Text {
id: desc
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: name.bottom
anchors.topMargin: 20
text: "A Simple Sailfish TOTP Generator<br />(RFC 6238 compatible)"
color: "white"
}
Text {
id: copyright
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: desc.bottom
anchors.topMargin: 20
text: "Copyright: Stefan Brand<br />License: BSD (3-clause)"
color: "white"
}
Button {
id: homepage
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: copyright.bottom
anchors.topMargin: 20
text: "<a href=\"https://github.com/seiichiro0185/sailotp\">SailOTP on Github</a>"
onClicked: {
Qt.openUrlExternally("https://github.com/seiichiro0185/sailotp")
}
}
Text {
id: accnowledgement
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: homepage.bottom
anchors.topMargin: 20
text: "SailOTP uses the SHA-1 Implementation<br />from http://caligatio.github.io/jsSHA/"
color: "white"
}
}
Text {
id: accnowledgement
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: homepage.bottom
anchors.topMargin: 20
text: "SailOTP uses the SHA-1 Implementation<br />from http://caligatio.github.io/jsSHA/"
color: "white"
}
}

View File

@ -30,52 +30,55 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
import "../lib/storage.js" as DB
import "../lib/storage.js" as DB // Import the storage library for Config-Access
// Define Layout of the Add OTP Dialog
Dialog {
id: addOTP
id: addOTP
property QtObject parentPage: null
// We get the Object of the parent page on call to refresh it after adding a new Entry
property QtObject parentPage: null
SilicaFlickable {
id: addOtpList
anchors.fill: parent
SilicaFlickable {
id: addOtpList
anchors.fill: parent
VerticalScrollDecorator {}
VerticalScrollDecorator {}
Column {
anchors.fill: parent
DialogHeader {
acceptText: "Add"
}
TextField {
id: otpLabel
width: parent.width
label: "Title"
placeholderText: "Title for the OTP"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
TextField {
id: otpSecret
width: parent.width
label: "Secret"
placeholderText: "Secret OTP Key"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
}
Column {
anchors.fill: parent
DialogHeader {
acceptText: "Add"
}
TextField {
id: otpLabel
width: parent.width
label: "Title"
placeholderText: "Title for the OTP"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
TextField {
id: otpSecret
width: parent.width
label: "Secret"
placeholderText: "Secret OTP Key"
focus: true
horizontalAlignment: TextInput.AlignLeft
}
}
}
onDone: {
if (otpLabel.text != "" && otpSecret.text != "") {
DB.addOTP(otpLabel.text, otpSecret.text);
parentPage.refreshOTPList();
}
// Save if page is Left with Add
onDone: {
// Some basic Input Check, we need both Values to work
if (otpLabel.text != "" && otpSecret.text != "") {
// Save the entry to the Config DB
DB.addOTP(otpLabel.text, otpSecret.text);
// Refresh the main Page
parentPage.refreshOTPList();
}
}
}

View File

@ -34,139 +34,147 @@ import "../lib/storage.js" as DB
import "../lib/crypto.js" as OTP
Page {
id: mainPage
id: mainPage
ListModel {
id: otpListModel
ListModel {
id: otpListModel
}
// This holds the time of the last update of the page as Unix Timestamp (in Milliseconds)
property double lastUpdated: null
// Add an entry to the list
function appendOTP(title, secret) {
otpListModel.append({"secret": secret, "title": title, "otp": ""});
}
// Reload the List of OTPs from storage
function refreshOTPList() {
otpListModel.clear();
DB.getOTP();
refreshOTPValues();
}
// Calculate new OTPs for every entry
function refreshOTPValues() {
// get seconds from current Date
var curDate = new Date();
var seconds = curDate.getSeconds();
// 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);
console.log("Updating Value ", i);
}
}
property double lastUpdated: null
// Update the Progressbar
updateProgress.value = 29 - (seconds % 30)
// Set lastUpdate property
lastUpdated = curDate.getTime();
}
// Add an entry to the list
function appendOTP(title, secret) {
otpListModel.append({"secret": secret, "title": title, "otp": ""});
Timer {
interval: 1000
running: Qt.application.active // Timer only runs when App is active
repeat: true
onTriggered: refreshOTPValues();
}
SilicaFlickable {
anchors.fill: parent
PullDownMenu {
MenuItem {
text: "About"
onClicked: pageStack.push(Qt.resolvedUrl("About.qml"))
}
MenuItem {
text: "Add OTP"
onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage})
}
}
// Reload the List of OTPs from storage
function refreshOTPList() {
otpListModel.clear();
DB.getOTP();
refreshOTPValues();
ProgressBar {
id: updateProgress
width: parent.width
maximumValue: 29
anchors.top: parent.top
anchors.topMargin: 48
}
// Calculate new OTPs for every entry
function refreshOTPValues() {
var curDate = new Date();
var seconds = curDate.getSeconds();
SilicaListView {
id: otpList
header: PageHeader {
title: "SailOTP"
}
anchors.fill: parent
model: otpListModel
width: parent.width
for (var i=0; i<otpListModel.count; i++) {
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);
console.log("Updating Value ", i);
}
ViewPlaceholder {
enabled: otpList.count == 0
text: "Nothing here"
hintText: "Pull down to add a OTP"
}
delegate: ListItem {
id: otpListItem
menu: otpContextMenu
width: otpList.width
contentHeight: Theme.itemSizeMedium
function remove() {
// Show 5s countdown, then delete from DB and List
remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) })
}
updateProgress.value = 29 - (seconds % 30)
lastUpdated = curDate.getTime();
}
ListView.onRemove: animateRemoval()
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
Timer {
interval: 1000
running: Qt.application.active
repeat: true
Label {
id: otpLabel
text: model.title
color: Theme.secondaryColor
anchors.horizontalCenter: parent.horizontalCenter
}
onTriggered: refreshOTPValues();
}
Label {
id: otpValue
anchors.top: otpLabel.bottom
text: model.otp
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
}
}
SilicaFlickable {
anchors.fill: parent
PullDownMenu {
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: "About"
onClicked: pageStack.push(Qt.resolvedUrl("About.qml"))
}
MenuItem {
text: "Add OTP"
onClicked: pageStack.push(Qt.resolvedUrl("AddOTP.qml"), {parentPage: mainPage})
text: "Delete"
onClicked: remove()
}
}
}
}
VerticalScrollDecorator{}
ProgressBar {
id: updateProgress
width: parent.width
maximumValue: 29
anchors.top: parent.top
anchors.topMargin: 48
}
SilicaListView {
id: otpList
header: PageHeader {
title: "SailOTP"
}
anchors.fill: parent
model: otpListModel
width: parent.width
ViewPlaceholder {
enabled: otpList.count == 0
text: "Nothing here"
hintText: "Pull down to add a OTP"
}
delegate: ListItem {
id: otpListItem
menu: otpContextMenu
width: otpList.width
contentHeight: Theme.itemSizeMedium
function remove() {
remorseAction("Deleting", function() { DB.removeOTP(title, secret); otpListModel.remove(index) })
}
ListView.onRemove: animateRemoval()
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
Label {
id: otpLabel
text: model.title
color: Theme.secondaryColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
id: otpValue
anchors.top: otpLabel.bottom
text: model.otp
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeLarge
}
}
Component {
id: otpContextMenu
ContextMenu {
MenuItem {
text: "Delete"
onClicked: remove()
}
}
}
}
VerticalScrollDecorator{}
Component.onCompleted: {
DB.initialize();
refreshOTPList();
}
}
Component.onCompleted: {
// Initialize DB (create tables etc..)
DB.initialize();
// Load list of OTP-Entries
refreshOTPList();
}
}
}
}

View File

@ -1,32 +1,31 @@
/*
Copyright (C) 2013 Jolla Ltd.
Contact: Thomas Perl <thomas.perl@jollamobile.com>
All rights reserved.
You may use this file under the terms of BSD license as follows:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Jolla Ltd nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
* Copyright (c) 2013, Stefan Brand <seiichiro@seiichiro0185.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The names of the contributors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef QT_QML_DEBUG
#include <QtQuick>
@ -34,9 +33,8 @@
#include <sailfishapp.h>
int main(int argc, char *argv[])
{
return SailfishApp::main(argc, argv);
return SailfishApp::main(argc, argv);
}