Initial revision.
[secretnotes] / src / mainwindow.cpp
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
new file mode 100644 (file)
index 0000000..835bde2
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ *  This file is part of Secret Notes.
+ *  Copyright (C) 2010 Janusz Sobczak
+ *
+ *  Secret Notes is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  Secret Notes is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with Secret Notes.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <QDir>
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include <qmessagebox.h>
+#include <qtimer.h>
+#include <qfile.h>
+#include <qdatastream.h>
+#include <qinputdialog.h>
+#include <openssl/blowfish.h>
+#include <openssl/sha.h>
+#include "passworddialog.h"
+
+const char filename[] = "secretnotes.scn";
+
+MainWindow::MainWindow(QWidget *parent) :
+    QMainWindow(parent),
+    ui(new Ui::MainWindow),
+    password(NULL),
+    hasPasswordChanged(false),
+    hasReadFile(false)
+{
+    ui->setupUi(this);
+    enableTextEdit(false);
+    undoResetAction = ui->menuOptions->addAction(tr("Undo reset"));
+    connect(undoResetAction, SIGNAL(triggered()),this, SLOT(undoReset()));
+    enableUndoReset(false);
+
+    undoEditAction = ui->menuOptions->addAction(tr("Undo edit"));
+    connect(undoEditAction,SIGNAL(triggered()), this, SLOT(undoEdit()));
+    on_textEdit_undoAvailable(false);
+
+    /* the secret notes file is stored in user home directory */
+    filePath = QDir::fromNativeSeparators(QDir::homePath());
+
+    /* or in $HOME/DOCUMENTS */
+#if defined(DOCUMENTS)
+#define DOC_DIR_(x) #x
+#define DOC_DIR(x) DOC_DIR_(x)
+    QString doc(filePath + QString("/") + QString(DOC_DIR(DOCUMENTS)));
+    QDir dir(doc);
+    if (dir.exists())
+        filePath = doc;
+#endif
+    QTimer::singleShot(1, this, SLOT(readFile()));
+}
+
+MainWindow::~MainWindow()
+{
+    plaintext.fill(0);
+    undoText.fill(0);
+    delete ui;
+    if (password)
+        memset(password,0,passLength);
+    delete[] password;
+}
+
+void MainWindow::changeEvent(QEvent *e)
+{
+    QMainWindow::changeEvent(e);
+    switch (e->type()) {
+    case QEvent::LanguageChange:
+        ui->retranslateUi(this);
+        break;
+    default:
+        break;
+    }
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+    QFile file(filePath + QString("/") + filename);
+    QString text = ui->textEdit->toPlainText();
+
+    /* don't save the file if content hasn't changed */
+    if (((text == plaintext) && (!hasPasswordChanged)) ||
+            (!ui->textEdit->isEnabled()) ||
+            (text.length() < 1)) {
+        text.fill(0);
+        return;
+    }
+
+    QByteArray encoded;
+    if (!password)
+        askNewPassword();
+
+    /* has user provided a password */
+    if (!password) {
+        text.fill(0);
+        return;
+    }
+
+    encode(encoded, text);
+    text.fill(0);
+
+    if (encoded.length() < 1)
+        return;
+
+    if (!file.open(QIODevice::WriteOnly)) {
+        QMessageBox::warning(this, tr("File open failed"),
+                             tr("Can't open %1").arg(filename),QMessageBox::Ok);
+    } else {
+        QDataStream stream(&file);
+        stream << QString("SECRETNOTES");
+        stream << (short int)1;
+        stream << encoded;
+        hasPasswordChanged = false;
+    }
+    text.fill(0);
+    encoded.fill(0);
+    event->accept();
+}
+
+void MainWindow::readFile()
+{
+    /* we want to call readFile only once */
+    if (hasReadFile)
+        return;
+    hasReadFile = true;
+
+    QFile file(filePath + QString("/") + filename);
+    if (!file.open(QIODevice::ReadOnly)) {
+        ui->textEdit->setPlainText("");
+        enableTextEdit(true);
+        askNewPassword();
+        QMessageBox::information(this,tr("Welcome"),
+                                 tr("Welcome to Secret Notes.\n"
+                                    "Your data will be saved automatically\n"
+                                    "when you close Secret Notes."));
+    } else {
+        QString decoded;
+        QString head;
+        short int version;
+        QByteArray text;
+        QDataStream stream(&file);
+        stream >> head;
+        stream >> version;
+        if ((head != QString("SECRETNOTES")) || (version != 1))
+            return;
+        stream >> text;
+        if (text.length() > 0) {
+            queryPassword(false);
+            if (!decode(decoded, text)) {
+                int i;
+                bool ok;
+                for (i=0; i<2; i++) {
+                    queryPassword(true);
+                    ok = decode(decoded, text);
+                    if (ok)
+                        break;
+                }
+                if (!ok)
+                    return;
+            }
+            plaintext.fill(0);
+            plaintext = decoded;
+            ui->textEdit->setPlainText(plaintext);
+            enableTextEdit(true);
+        }
+        decoded.fill(0);
+    }
+}
+
+void MainWindow::queryPassword(bool retry)
+{
+    if (password && !retry)
+        return;
+    bool ok;
+    QString pass = QInputDialog::getText(this,
+                                         tr("Password"),
+                                         tr("Enter passphrase:"),
+                                         QLineEdit::PasswordEchoOnEdit,"", &ok);
+    if (ok) {
+        setPassword(pass);
+        pass.fill(0);
+    }
+}
+
+bool MainWindow::decode(QString &output, const QByteArray &input)
+{
+    int size = input.length();
+    int length = size - 8 - SHA_DIGEST_LENGTH;
+    if (length < 0)
+        return false;
+    if (!password)
+        return false;
+    char *out = new char[size+1];
+    out[size] = 0;
+
+    blowfish((unsigned char*)out, (unsigned char*)input.constData(), size, 0);
+
+    const char *text = out + 8 + SHA_DIGEST_LENGTH;
+    /* verify SHA checksum */
+    unsigned char *md =
+            SHA1((unsigned char *)text, length, NULL);
+    unsigned char *digest = (unsigned char *)out + 8;
+    for (int i=0; i<SHA_DIGEST_LENGTH; i++) {
+        if (md[i] != digest[i]) {
+            memset(out, 0, size);
+            delete[] out;
+            return false;
+        }
+    }
+
+    output = QString::fromUtf8(qUncompress(QByteArray(text, length)));
+    memset(out, 0, size);
+    delete[] out;
+    return true;
+}
+
+void MainWindow::encode(QByteArray &output, const QString &input)
+{
+    QByteArray bytes = qCompress(input.toUtf8());
+    int length = bytes.size();
+    int size = 8 + SHA_DIGEST_LENGTH + length;
+    QByteArray in;
+    char *out = new char[size];
+    int i;
+
+    in.reserve(size);
+    /* add 8 psuedo random bytes */
+    qsrand((uint)&bytes + size);
+    for(i=0; i<8; i++)
+        in.append((char)qrand());
+
+    /* add SHA checksum */
+    unsigned char *md = SHA1((unsigned char*)bytes.constData(), length, NULL);
+    for(i=0; i<SHA_DIGEST_LENGTH; i++)
+        in.append((char)md[i]);
+
+    /* add input text */
+    in.append(bytes);
+
+    blowfish((unsigned char*)out, (unsigned char*)in.constData(),  size, 1);
+    output = QByteArray(out, size);
+    bytes.fill(0);
+    in.fill(0);
+    delete[] out;
+}
+
+void MainWindow::blowfish(unsigned char *output, const unsigned char *input,
+                          int length, int enc)
+{
+    BF_KEY key;
+    unsigned char ivec[8];
+    int num = 0;
+
+    BF_set_key(&key, passLength, password);
+    BF_cfb64_encrypt(input, output, length, &key, ivec, &num, enc);
+    memset(&key, 0, sizeof(BF_KEY));
+}
+
+void MainWindow::on_actionChange_password_triggered()
+{
+    PasswordDialog dialog(this, (char*)password);
+    if (dialog.exec() == QDialog::Accepted) {
+        QString pass = dialog.getPassword();
+        setPassword(pass);
+        pass.fill(0);
+    }
+}
+
+void MainWindow::on_actionReset_secret_notes_triggered()
+{
+    if (QMessageBox::question(this, tr("Reset secret notes"),
+                              tr("Do you want to reset Secret Notes?\n"
+                                 "This will destroy all contents!"),
+                              QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
+        return;
+    }
+    if (password)
+        memset(password, 0, passLength);
+    delete[] password;
+    password = NULL;    
+    if (plaintext.length() > 0) {
+        undoText.fill(0);
+        undoText = plaintext;
+    }
+
+    if (undoText.length() > 0)
+        enableUndoReset(true);
+    ui->textEdit->clear();
+    enableTextEdit(true);
+    askNewPassword();
+}
+
+void MainWindow::setPassword(const QString &text)
+{
+    int length = text.length();
+    if (length > 0) {
+        if (password)
+            memset(password, 0, passLength);
+        delete[] password;
+        password = new unsigned char[length+1];
+        int i;
+        for(i=0; i<length; i++)
+            password[i] = text.constData()[i].toAscii();
+        password[i] = 0;
+        passLength = length;
+        hasPasswordChanged = true;
+    }
+}
+
+void MainWindow::askNewPassword()
+{
+    int retries = 3;
+    PasswordDialog dialog(this);
+    dialog.hideOldPassword(true);
+    while (retries--) {
+        if (dialog.exec() == QDialog::Accepted) {
+            QString pass = dialog.getPassword();
+            setPassword(pass);
+            pass.fill(0);
+            return;
+        }
+    }
+}
+
+void MainWindow::enableTextEdit(bool ena)
+{
+    ui->textEdit->setEnabled(ena);
+    if (!ena) {
+        ui->textEdit->setPlainText(tr("Secret Notes disabled"));
+    }
+}
+
+void MainWindow::enableUndoReset(bool ena)
+{
+    undoResetAction->setVisible(ena);
+}
+
+void MainWindow::undoReset()
+{
+    plaintext.fill(0);
+    plaintext = undoText;
+    undoText.fill(0);
+    undoText = "";
+    ui->textEdit->setPlainText(plaintext);
+    enableUndoReset(false);
+    enableTextEdit(true);
+}
+
+void MainWindow::undoEdit()
+{
+    ui->textEdit->undo();
+}
+
+void MainWindow::on_textEdit_undoAvailable(bool b)
+{
+    undoEditAction->setVisible(b);
+}