четверг, 4 июля 2013 г.

Пароли: безопасный ввод и хранение

Перейду сразу к рассмотрению примера. Допустим, у нас есть некий сервер, к которому подключаются пользователи. Для идентификации используются логин и пароль. Клиентами могут быть как консольные приложения, так и приложения с графическим интерфейсом. Задача: организовать безопасный ввод пароля, а также не менее безопасные хранение (как на сервере, так и в настройках клиента) и передачу этого пароля на сервер. Что ж, приступим.

Начнём с хранения на сервере и передачи. Тут всё достаточно просто: пароль нужно хранить и передавать в зашифрованном виде. Вполне подойдёт, например, алгоритм SHA-1. В Qt есть замечательный класс QCryptographicHash, позволяющий зашифровать пароль при помощи выбранного алгоритма, в том числе и SHA-1. Как это выглядит на практике:
 QString pwd;  
 //вводим пароль  
 QByteArray encryptedPwd = QCryptographicHash::hash(pwd.toUtf8(), QCryptographicHash::Sha1);  
Готово, пароль зашифрован. В таком виде мы его и будем передавать на сервер (скажем, при помощи QTcpSocket) и хранить там, например, в базе данных:
 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");  
 //задаем нужные параметры, такие как имя файла или адрес сервера  
 QSqlQuery q(db);  
 q.prepare("INSERT INTO users (login, password) VALUES (:login, :password)");  
 //биндим логин и прочие необходимые данные пользователя  
 q.bindValue(":password", encryptedPwd); //биндим наш пароль  
 q.exec();  
Теперь в нашей базе хранится пароль в зашифрованном виде. О базах данных и о работе с сетью в Qt много и доходчиво написано в официальной документации, не буду заниматься дублированием.

Поговорим о вводе пароля и его хранении в настройках клиента (вряд ли кому-то приятно каждый раз при открытии приложения заново вводить пароль). Тут в целом всё довольно просто. Используем QSettings:
 QSettings s;  
 s.setValue("password", encryptedPassword);  
Вот и всё. Но есть один нюанс, о котором будет сказано дальше. Пока же перейдём к вводу.

Начнём, пожалуй, с ввода из консоли (или из терминала, кому как больше нравится). Казалось бы, делаем std::cin >> pwd и радуемся жизни, но не тут то было. Если вы хоть раз пользовались консольными приложениями, требующими ввода пароля, то скорее всего помните, что при его вводе символы, которые вы печатали, не отображались в окне. Сделано это, как нетрудно догадаться, для того, чтобы никто не подсмотрел пароль. Попробуем сделать так же? Попробуем:
 #include <cstdio>  
   
 #if defined(Q_OS_LINUX) || defined(Q_OS_MAC)  
 #include "termios.h"  
 #elif defined(Q_OS_WIN)  
 #include "windows.h"  
 #endif  
   
 void setStdinEchoEnabled(bool enabled)  
 {  
 #if defined(Q_OS_MAC) || defined(Q_OS_LINUX)  
   struct termios tty;  
   tcgetattr(STDIN_FILENO, &tty);  
   if(enabled)  
     tty.c_lflag |= ECHO;  
   else  
     tty.c_lflag &= ~ECHO;  
   tcsetattr(STDIN_FILENO, TCSANOW, &tty);  
 #elif defined(Q_OS_WIN)  
   HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);  
   DWORD mode;  
   GetConsoleMode(hStdin, &mode);  
   if(enabled)  
     mode |= ENABLE_ECHO_INPUT;  
   else  
     mode &= ~ENABLE_ECHO_INPUT;  
   SetConsoleMode(hStdin, mode);  
 #endif  
 }  
Выглядит достаточно монструозно, давайте немного разберёмся. Назначение функции довольно простое: она включает или отключает отображение вводимых в консоль символов. Директивы #define нужны потому, что на разных платформах это делается по-разному (и подключаемые заголовки тоже разные). Подробнее об использованных структурах и функциях можно почитать в интернете, а пока посмотрим, как это использовать на практике:
 std::cout << "Enter password: ";  
 setStdinEchoEnabled(false);  
 std::string pwd;  
 std::cin >> pwd;  
 setStdinEchoEnabled(true);  
 std::cout << "\n";  
Последняя строчка нужна для того, чтобы ввести "съеденный" при вводе пароля с отключённым "эхом" символ конца строки.

Чтобы не возиться с std::cin и std::cout, можно ввести следующие функции:
 void write(const QString &s)  
 {  
   static QTextStream out(stdout, QIODevice::WriteOnly);  
   out << s;  
   out.flush();  
 }  
   
 QString readLine()  
 {  
   static QTextStream in(stdin, QIODevice::ReadOnly);  
   return in.readLine();  
 }  
Подробнее о QTextStream и его использовании написано в документации. Теперь ввод пароля может выглядеть так:
 write("Enter password: ");  
 setStdinEchoEnabled(false);  
 QString pwd = readLine();  
 setStdinEchoEnabled(true);  
 write("\n");  
На мой взгляд, это чуточку проще и понятнее, а также избавляет от необходимости производить преобразования между типами стандартной библиотеки C++ и типами Qt.

А теперь перейдём к последнему и самому, как мне кажется, интересному — вводу пароля с использованием графического интерфейса. За основу возьмём QLineEdit. Создадим следующий класс:
 class PasswordWidget : public QWidget  
 {  
   Q_OBJECT  
 private:  
   QByteArray encPwd;  
   QLineEdit *ledt;  
 public:  
   explicit PasswordWidget(QWidget *parent = 0)  
   {  
     QHBoxLayout *hlt = new QHBoxLayout(this);  
     hlt->setContentsMargins(0, 0, 0, 0);  
     ledt = new QLineEdit;  
     ledt->setEchoMode(QLineEdit::Password);  
     connect(ledt, SIGNAL(textChanged(QString)), this, SIGNAL(passwordChanged()));  
     hlt->addWidget(ledt);  
   }  
 public:  
   void setPassword(const QString &pwd)  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->setText(pwd);  
   }  
   void setEncryptedPassword(const QByteArray &pwd, int charCountHint = 0)  
   {  
     encPwd = pwd;  
     ledt->clear();  
     ledt->setPlaceholderText(QString().fill('*', (charCountHint > 0) ? charCountHint : 0));  
   }  
   void clear()  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->clear();  
   }  
   void restoreState(const QByteArray &ba)  
   {  
     QDataStream in(ba);  
     bool enc = false;  
     in >> enc;  
     if (enc)  
     {  
       QByteArray pwd;  
       int cc = 0;  
       in >> pwd;  
       in >> cc;  
       setEncryptedPassword(pwd, cc);  
     }  
     else  
     {  
       QString pwd;  
       in >> pwd;  
       setPassword(pwd);  
     }  
   }  
   QString password() const  
   {  
     return ledt->text();  
   }  
   QByteArray encryptedPassword(QCryptographicHash::Algorithm a) const  
   {  
     if (!ledt->text().isEmpty())  
       return QCryptographicHash::hash(ledt->text().toUtf8(), a);  
     else if (!encPwd.isEmpty())  
       return encPwd;  
     else  
       return QByteArray();  
   }  
   int сharCountHint() const  
   {  
     return ledt->placeholderText().length();  
   }  
   QByteArray saveState() const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << false;  
     out << password();  
     return ba;  
   }  
   QByteArray saveStateEncrypted(QCryptographicHash::Algorithm method) const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << true;  
     out << encryptedPassword(a);  
     out << charCountHint();  
     return ba;  
   }  
 signals:  
   void passwordChanged();  
 };  
Посмотрим, что тут к чему. Во-первых, мы инкапсулировали QLineEdit внутри нашего класса, чтобы к нему не было прямого доступа (например, чтобы нельзя было менять текст). Далее, мы сделали так, чтобы вместо реальных символов всегда отображались звёздочки. Пароль при этом можно задавать не только в явном ("как есть"), но и в зашифрованном виде. Во втором случае можно также указать, из скольких символов состоит пароль, чтобы отображалось именно столько звёздочек (charCountHint). Сигнал passwordChanged() оповещает о том, что пароль был каким-либо образом изменён (программно, либо при вводе или удалении символов пользователем).

Теперь о главном — о хранении. Метод saveStateEncrypted() сериализует параметры пароля в массив байтов. Указывается тип пароля (зашифрованный или явный), а также собственно пароль и, в случае, если он зашифрован, подсказка о количестве символов. Десериализация происходит с точностью до наоборот. Посмотрим, как это применить на практике:
 //создание виджета  
 PasswordWidget *pwgt = new PasswordWidget;  
   
 //сохранение пароля при закрытии приложения  
 QSettings s;  
 s.setValue("password", pwgt->saveStateEncrypted(QCryptographicHash::Sha1));  
   
 //восстановление пароля при открытии приложения  
 QSettings s;  
 pwgt->restoreState(s.value("password").toByteArray());  
Вот, собственно, и всё. Для большинства приложений, где не требуются крайние меры по защите информации, приведённых способов работы с паролями должно хватить. Имеющиеся функции и класс PasswordWidget, конечно же, могут быть улучшены, дополнены разными возможностями, но это уже, как говорится, совсем другая история.

Комментариев нет:

Отправить комментарий