diff --git a/qml/components/NotifyBanner.qml b/qml/components/NotifyBanner.qml index ec60069..3274ec3 100644 --- a/qml/components/NotifyBanner.qml +++ b/qml/components/NotifyBanner.qml @@ -63,7 +63,6 @@ MouseArea { id: notifyText anchors.left: parent.left anchors.right: parent.right - anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter anchors.margins: Theme.paddingLarge diff --git a/qml/lib/storage.js b/qml/lib/storage.js index e147599..a866812 100644 --- a/qml/lib/storage.js +++ b/qml/lib/storage.js @@ -97,11 +97,12 @@ function db2json() { } // Read Values from JSON and put them into the DB -function json2db(jsonString) { +function json2db(jsonString, error) { var json = JSON.parse(jsonString); + error = ""; if (json.version != "1" && json.app != "sailotp" ) { - console.log("Unrecognized JSON format"); + error = "Unrecognized format, file is not a SailOTP export"; return(false); } else { var otpList = []; @@ -114,8 +115,9 @@ function json2db(jsonString) { } } parentPage.refreshOTPList(); + return(true); } else { - console.log("File contains no Items"); + error = "File contains no Tokens"; return(false); } } diff --git a/qml/pages/ExportPage.qml b/qml/pages/ExportPage.qml index bfe656f..f496e70 100644 --- a/qml/pages/ExportPage.qml +++ b/qml/pages/ExportPage.qml @@ -40,14 +40,44 @@ Dialog { // We get the Object of the parent page on call to refresh it after adding a new Entry property QtObject parentPage: null - property string mode: "import" + function fillNum(num) { + if (num < 10) { + return("0"+num); + } else { + return(num) + } + } + + function creFileName() { + var date = new Date(); + return("/home/nemo/sailotp_"+date.getFullYear()+fillNum(date.getMonth()+1)+fillNum(date.getDate())+".aes"); + } + + function checkFileName(file) { + if (mode == "export") { + if (exportFile.exists(file) && !fileOverwrite.checked) { + notify.show("File already exists, choose \"Overwrite existing\" to overwrite it.", 4000); + return(false) + } else { + return(true) + } + } else { + if (exportFile.exists(file)) { + return(true) + } else { + notify.show("Given file does not exist!", 4000); + return(false) + } + } + } + // FileIO Object for reading / writing files FileIO { id: exportFile source: fileName.text - onError: console.log(msg) + onError: { console.log(msg); } } SilicaFlickable { @@ -62,47 +92,124 @@ Dialog { acceptText: mode == "export" ? "Export" : "Import" } - /*ComboBox { - id: modeSel - label: "Mode: " - menu: ContextMenu { - MenuItem { text: "Export"; onClicked: { mode = "export" } } - MenuItem { text: "Import"; onClicked: { mode = "import" } } - } - }*/ TextField { id: fileName width: parent.width + text: mode == "export" ? creFileName() : "/home/nemo/"; label: "Filename" - placeholderText: "File to Export / Import" + placeholderText: mode == "import" ? "File to import" : "File to export" focus: true horizontalAlignment: TextInput.AlignLeft } + + TextSwitch { + id: fileOverwrite + checked: false + visible: mode == "export" + text: "Overwrite existing" + } + TextField { id: filePassword width: parent.width label: "Password" - placeholderText: "Password for the Export" + placeholderText: "Password for the file" echoMode: TextInput.Password focus: true horizontalAlignment: TextInput.AlignLeft } + + TextField { + id: filePasswordCheck + width: parent.width + label: (filePassword.text != filePasswordCheck.text && filePassword.text.length > 0) ? "Passwords don't match!" : "Passwords match!" + placeholderText: "Repeated Password for the file" + visible: mode == "export" + echoMode: TextInput.Password + focus: true + horizontalAlignment: TextInput.AlignLeft + } + + Text { + id: importText + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 20 + width: parent.width - 2*Theme.paddingLarge + + wrapMode: Text.Wrap + maximumLineCount: 15 + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + + visible: mode == "import" + text: "Here you can Import Tokens from a file. Put in the file location and the password you used on export. Pull left to start the import." + } + + Text { + id: exportText + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 20 + width: parent.width - 2*Theme.paddingLarge + + wrapMode: Text.Wrap + maximumLineCount: 15 + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + + visible: mode == "export" + text: "Here you can export Tokens to a file. The exported file will be encrypted with AES-256-CBC and Base64 encoded. Choose a strong password, the file will contain the secrets used to generate the Tokens for your accounts. Pull left to start the export." + } } } // Check if we can continue - canAccept: fileName.text.length > 0 && filePassword.text.length > 0 ? true : false + canAccept: fileName.text.length > 0 && filePassword.text.length > 0 && (mode == "import" || filePassword.text == filePasswordCheck.text) && checkFileName(fileName.text) ? true : false // Do the DB-Export / Import - // TODO: Error handling and enctyption onDone: { if (result == DialogResult.Accepted) { + var plainText = "" + var chipherText = "" + if (mode == "export") { - console.log("Exporting to " + fileName.text); - exportFile.write(Gibberish.AES.enc(DB.db2json(), filePassword.text)); + // Export Database to File + plainText = DB.db2json(); + + if (plainText != "") { + try { + chipherText = Gibberish.AES.enc(plainText, filePassword.text); + if (!exportFile.write(chipherText)) { + notify.show("Error writing to file "+ fileName.text, 4000); + } else { + notify.show("Token Database exported to "+ fileName.text, 4000); + } + } catch(e) { + notify.show("Could not encrypt tokens. Error: ", 4000); + } + } else { + notify.show("Could not read tokens from Database", 4000); + } } else if(mode == "import") { - console.log("Importing ftom " + fileName.text); - DB.json2db(Gibberish.AES.dec(exportFile.read(), filePassword.text)) + // Import Tokens from File + + chipherText = exportFile.read(); + if (chipherText != "") { + try { + var errormsg = "" + plainText = Gibberish.AES.dec(chipherText, filePassword.text); + if (DB.json2db(plainText, errormsg)) { + notify.show("Tokens imported from "+ fileName.text, 4000); + } else { + notify.show(errormsg, 4000); + } + } catch (e) { + notify.show("Unable to decrypt file, did you use the right password?", 4000); + } + } else { + notify.show("Could not read from file " + fileName.text, 4000); + } } } } diff --git a/qml/pages/MainView.qml b/qml/pages/MainView.qml index bb58f35..7849f89 100644 --- a/qml/pages/MainView.qml +++ b/qml/pages/MainView.qml @@ -163,7 +163,7 @@ Page { onClicked: { Clipboard.text = otp - notify.show("Token for " + title + " copied", 3000); + notify.show("Token for " + title + " copied to clipboard", 3000); } ListView.onRemove: animateRemoval() diff --git a/src/fileio.cpp b/src/fileio.cpp index eceea0b..9793d60 100644 --- a/src/fileio.cpp +++ b/src/fileio.cpp @@ -49,3 +49,25 @@ bool FileIO::write(const QString& data) return true; } + +bool FileIO::exists() +{ + if (mSource.isEmpty()) { + emit error("Source is empty!"); + return false; + } + + QFile file(mSource); + return file.exists(); +} + +bool FileIO::exists(const QString& filename) +{ + if (filename.isEmpty()) { + emit error("Source is empty!"); + return false; + } + + QFile file(filename); + return file.exists(); +} diff --git a/src/fileio.h b/src/fileio.h index 011b99b..e8c67c2 100644 --- a/src/fileio.h +++ b/src/fileio.h @@ -16,6 +16,8 @@ public: Q_INVOKABLE QString read(); Q_INVOKABLE bool write(const QString& data); + Q_INVOKABLE bool exists(); + Q_INVOKABLE bool exists(const QString& filename); QString source() { return mSource; };