Modified Reset menu action behaviour.
[secretnotes] / src / mainwindow.cpp
1 /*
2  *  This file is part of Secret Notes.
3  *  Copyright (C) 2010 Janusz Sobczak
4  *
5  *  Secret Notes is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  Secret Notes is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with Secret Notes.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 #include <QDir>
19 #include "mainwindow.h"
20 #include "ui_mainwindow.h"
21 #include <qmessagebox.h>
22 #include <qtimer.h>
23 #include <qfile.h>
24 #include <qdatastream.h>
25 #include <qinputdialog.h>
26 #include <openssl/blowfish.h>
27 #include <openssl/sha.h>
28 #include "passworddialog.h"
29
30 const char filename[] = "secretnotes.scn";
31
32 MainWindow::MainWindow(QWidget *parent) :
33     QMainWindow(parent),
34     ui(new Ui::MainWindow),
35     password(NULL),
36     hasPasswordChanged(false),
37     hasReadFile(false)
38 {
39     ui->setupUi(this);
40
41     resetAction = ui->menuOptions->addAction(tr("Reset secret notes"));
42     connect(resetAction, SIGNAL(triggered()), this, SLOT(on_actionReset_secret_notes_triggered()));
43
44     enableTextEdit(false);
45
46     undoEditAction = ui->menuOptions->addAction(tr("Undo edit"));
47     connect(undoEditAction,SIGNAL(triggered()), this, SLOT(undoEdit()));
48     on_textEdit_undoAvailable(false);
49
50     /* the secret notes file is stored in user home directory */
51     filePath = QDir::fromNativeSeparators(QDir::homePath());
52
53     /* or in $HOME/DOCUMENTS */
54 #if defined(DOCUMENTS)
55 #define DOC_DIR_(x) #x
56 #define DOC_DIR(x) DOC_DIR_(x)
57     QString doc(filePath + QString("/") + QString(DOC_DIR(DOCUMENTS)));
58     QDir dir(doc);
59     if (dir.exists())
60         filePath = doc;
61 #endif
62     QTimer::singleShot(1, this, SLOT(readFile()));
63 }
64
65 MainWindow::~MainWindow()
66 {
67     plaintext.fill(0);
68     delete ui;
69     if (password)
70         memset(password,0,passLength);
71     delete[] password;
72 }
73
74 void MainWindow::changeEvent(QEvent *e)
75 {
76     QMainWindow::changeEvent(e);
77     switch (e->type()) {
78     case QEvent::LanguageChange:
79         ui->retranslateUi(this);
80         break;
81     default:
82         break;
83     }
84 }
85
86 void MainWindow::closeEvent(QCloseEvent *event)
87 {
88     QFile file(filePath + QString("/") + filename);
89     QString text = ui->textEdit->toPlainText();
90
91     /* don't save the file if content hasn't changed */
92     if (((text == plaintext) && (!hasPasswordChanged)) ||
93             (!ui->textEdit->isEnabled()) ||
94             (text.length() < 1)) {
95         text.fill(0);
96         return;
97     }
98
99     QByteArray encoded;
100     if (!password)
101         askNewPassword();
102
103     /* has user provided a password */
104     if (!password) {
105         text.fill(0);
106         return;
107     }
108
109     encode(encoded, text);
110     text.fill(0);
111
112     if (encoded.length() < 1)
113         return;
114
115     if (!file.open(QIODevice::WriteOnly)) {
116         QMessageBox::warning(this, tr("File open failed"),
117                              tr("Can't open %1").arg(filename),QMessageBox::Ok);
118     } else {
119         QDataStream stream(&file);
120         stream << QString("SECRETNOTES");
121         stream << (short int)1;
122         stream << encoded;
123         hasPasswordChanged = false;
124     }
125     text.fill(0);
126     encoded.fill(0);
127     event->accept();
128 }
129
130 void MainWindow::readFile()
131 {
132     /* we want to call readFile only once */
133     if (hasReadFile)
134         return;
135     hasReadFile = true;
136
137     QFile file(filePath + QString("/") + filename);
138     if (!file.open(QIODevice::ReadOnly)) {
139         ui->textEdit->setPlainText("");
140         enableTextEdit(true);
141         askNewPassword();
142         QMessageBox::information(this,tr("Welcome"),
143                                  tr("Welcome to Secret Notes.\n"
144                                     "Your data will be saved automatically\n"
145                                     "when you close Secret Notes."));
146     } else {
147         QString decoded;
148         QString head;
149         short int version;
150         QByteArray text;
151         QDataStream stream(&file);
152         stream >> head;
153         stream >> version;
154         if ((head != QString("SECRETNOTES")) || (version != 1))
155             return;
156         stream >> text;
157         if (text.length() > 0) {
158             queryPassword(false);
159             if (!decode(decoded, text)) {
160                 int i;
161                 bool ok;
162                 for (i=0; i<2; i++) {
163                     queryPassword(true);
164                     ok = decode(decoded, text);
165                     if (ok)
166                         break;
167                 }
168                 if (!ok)
169                     return;
170             }
171             plaintext.fill(0);
172             plaintext = decoded;
173             ui->textEdit->setPlainText(plaintext);
174             enableTextEdit(true);
175         }
176         decoded.fill(0);
177     }
178 }
179
180 void MainWindow::queryPassword(bool retry)
181 {
182     if (password && !retry)
183         return;
184     bool ok;
185     QString pass = QInputDialog::getText(this,
186                                          tr("Password"),
187                                          tr("Enter passphrase:"),
188                                          QLineEdit::PasswordEchoOnEdit,"", &ok);
189     if (ok) {
190         setPassword(pass);
191         pass.fill(0);
192     }
193 }
194
195 bool MainWindow::decode(QString &output, const QByteArray &input)
196 {
197     int size = input.length();
198     int length = size - 8 - SHA_DIGEST_LENGTH;
199     if (length < 0)
200         return false;
201     if (!password)
202         return false;
203     char *out = new char[size+1];
204     out[size] = 0;
205
206     blowfish((unsigned char*)out, (unsigned char*)input.constData(), size, 0);
207
208     const char *text = out + 8 + SHA_DIGEST_LENGTH;
209     /* verify SHA checksum */
210     unsigned char *md =
211             SHA1((unsigned char *)text, length, NULL);
212     unsigned char *digest = (unsigned char *)out + 8;
213     for (int i=0; i<SHA_DIGEST_LENGTH; i++) {
214         if (md[i] != digest[i]) {
215             memset(out, 0, size);
216             delete[] out;
217             return false;
218         }
219     }
220
221     output = QString::fromUtf8(qUncompress(QByteArray(text, length)));
222     memset(out, 0, size);
223     delete[] out;
224     return true;
225 }
226
227 void MainWindow::encode(QByteArray &output, const QString &input)
228 {
229     QByteArray bytes = qCompress(input.toUtf8());
230     int length = bytes.size();
231     int size = 8 + SHA_DIGEST_LENGTH + length;
232     QByteArray in;
233     char *out = new char[size];
234     int i;
235
236     in.reserve(size);
237     /* add 8 psuedo random bytes */
238     qsrand((uint)&bytes + size);
239     for(i=0; i<8; i++)
240         in.append((char)qrand());
241
242     /* add SHA checksum */
243     unsigned char *md = SHA1((unsigned char*)bytes.constData(), length, NULL);
244     for(i=0; i<SHA_DIGEST_LENGTH; i++)
245         in.append((char)md[i]);
246
247     /* add input text */
248     in.append(bytes);
249
250     blowfish((unsigned char*)out, (unsigned char*)in.constData(),  size, 1);
251     output = QByteArray(out, size);
252     bytes.fill(0);
253     in.fill(0);
254     delete[] out;
255 }
256
257 void MainWindow::blowfish(unsigned char *output, const unsigned char *input,
258                           int length, int enc)
259 {
260     BF_KEY key;
261     unsigned char ivec[8];
262     int num = 0;
263
264     BF_set_key(&key, passLength, password);
265     BF_cfb64_encrypt(input, output, length, &key, ivec, &num, enc);
266     memset(&key, 0, sizeof(BF_KEY));
267 }
268
269 void MainWindow::on_actionChange_password_triggered()
270 {
271     PasswordDialog dialog(this, (char*)password);
272     if (dialog.exec() == QDialog::Accepted) {
273         QString pass = dialog.getPassword();
274         setPassword(pass);
275         pass.fill(0);
276     }
277 }
278
279 void MainWindow::on_actionReset_secret_notes_triggered()
280 {
281     if (QMessageBox::question(this, tr("Reset secret notes"),
282                               tr("Do you want to reset Secret Notes?\n"
283                                  "This will destroy all contents!"),
284                               QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
285         return;
286     }
287     if (password)
288         memset(password, 0, passLength);
289     delete[] password;
290     password = NULL;
291
292     ui->textEdit->clear();
293     enableTextEdit(true);
294     askNewPassword();
295 }
296
297 void MainWindow::setPassword(const QString &text)
298 {
299     int length = text.length();
300     if (length > 0) {
301         if (password)
302             memset(password, 0, passLength);
303         delete[] password;
304         password = new unsigned char[length+1];
305         int i;
306         for(i=0; i<length; i++)
307             password[i] = text.constData()[i].toAscii();
308         password[i] = 0;
309         passLength = length;
310         hasPasswordChanged = true;
311     }
312 }
313
314 void MainWindow::askNewPassword()
315 {
316     int retries = 3;
317     PasswordDialog dialog(this);
318     dialog.hideOldPassword(true);
319     while (retries--) {
320         if (dialog.exec() == QDialog::Accepted) {
321             QString pass = dialog.getPassword();
322             setPassword(pass);
323             pass.fill(0);
324             return;
325         }
326     }
327 }
328
329 void MainWindow::enableTextEdit(bool ena)
330 {
331     ui->textEdit->setEnabled(ena);
332     if (!ena) {
333         ui->textEdit->setPlainText(tr("Secret Notes disabled"));
334     }
335     enableResetAction(!ena);
336 }
337
338
339 void MainWindow::undoEdit()
340 {
341     ui->textEdit->undo();
342 }
343
344 void MainWindow::on_textEdit_undoAvailable(bool b)
345 {
346     undoEditAction->setVisible(b);
347 }
348
349 void MainWindow::enableResetAction(bool b)
350 {
351     resetAction->setVisible(b);
352 }