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