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