You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.0 KiB
187 lines
5.0 KiB
#include <QHBoxLayout>
|
|
#include <QVBoxLayout>
|
|
#include <QToolButton>
|
|
#include <QAction>
|
|
#include <QSpacerItem>
|
|
#include <QInputDialog>
|
|
#include <QRegularExpression>
|
|
#include <QMessageBox>
|
|
#include <QSettings>
|
|
#include <QCoreApplication>
|
|
#include <QTimer>
|
|
#include <QScrollArea>
|
|
#include <QLabel>
|
|
#include "mainwindow.h"
|
|
#include "libcppotp/bytes.h"
|
|
#include "libcppotp/otp.h"
|
|
|
|
extern "C" {
|
|
#include <time.h>
|
|
}
|
|
|
|
#define GROUP_NAME "keys"
|
|
#define TIMER_INTERVAL_MS 1000
|
|
|
|
MainWindow::MainWindow(QWidget *parent) :
|
|
QMainWindow(parent)
|
|
,m_centralWidget(nullptr)
|
|
,m_settings(qApp->applicationName() + ".ini", QSettings::IniFormat)
|
|
,m_statusLabel(nullptr)
|
|
,m_timer(new QTimer())
|
|
{
|
|
m_settings.setIniCodec("UTF-8");
|
|
|
|
if(!m_settings.isWritable()) {
|
|
QMessageBox::critical(0, tr("Error"), tr("Permission denied while trying to write to settings file."));
|
|
qApp->quit();
|
|
return;
|
|
}
|
|
|
|
QByteArray savedGeometry = m_settings.value("main/geometry").toByteArray();
|
|
|
|
if(savedGeometry.size() > 0)
|
|
restoreGeometry(savedGeometry);
|
|
|
|
QTimer::singleShot(0, this, &MainWindow::rebuildLayout);
|
|
|
|
connect(this, &MainWindow::addKeyClicked, this, &MainWindow::onAddKeyClicked);
|
|
|
|
connect(m_timer, &QTimer::timeout, this, [this]() {
|
|
if(!m_statusLabel)
|
|
return;
|
|
|
|
unsigned secs = 30 - (time(NULL) % 30);
|
|
QString remaining = QString::number(secs) + " seconds remaining.";
|
|
|
|
m_statusLabel->setText(remaining);
|
|
|
|
if(secs == 30)
|
|
QTimer::singleShot(0, this, &MainWindow::rebuildLayout);
|
|
});
|
|
}
|
|
|
|
MainWindow::~MainWindow() {
|
|
m_settings.setValue("main/geometry", saveGeometry());
|
|
m_settings.sync();
|
|
}
|
|
|
|
void MainWindow::rebuildLayout() {
|
|
if(m_timer)
|
|
m_timer->stop();
|
|
|
|
if(m_statusLabel)
|
|
delete m_statusLabel;
|
|
|
|
if(m_centralWidget)
|
|
delete m_centralWidget;
|
|
|
|
// default layout is a vertical box
|
|
m_centralWidget = new QWidget();
|
|
setCentralWidget(m_centralWidget);
|
|
|
|
QVBoxLayout *vbox = new QVBoxLayout();
|
|
m_centralWidget->setLayout(vbox);
|
|
|
|
// main vbox layout for codes
|
|
QWidget *codeWidget = new QWidget();
|
|
QVBoxLayout *codeVBoxLayout = new QVBoxLayout();
|
|
|
|
codeWidget->setLayout(codeVBoxLayout);
|
|
|
|
QScrollArea *scrollArea = new QScrollArea();
|
|
scrollArea->setWidget(codeWidget);
|
|
scrollArea->setWidgetResizable(true);
|
|
|
|
vbox->addWidget(scrollArea);
|
|
|
|
// generate each code for keys in settings file and add to layout
|
|
m_settings.beginGroup(GROUP_NAME);
|
|
|
|
QStringList names = m_settings.childKeys();
|
|
names.sort(Qt::CaseInsensitive);
|
|
|
|
for(int i = 0; i < names.size(); ++i) {
|
|
QString name = names.at(i);
|
|
QString key = m_settings.value(name).toString();
|
|
QString code = generateCode(key);
|
|
|
|
QLabel *label = new QLabel(name + ": " + code, 0);
|
|
codeVBoxLayout->addWidget(label);
|
|
}
|
|
|
|
m_settings.endGroup();
|
|
|
|
// status bar at the bottom
|
|
QHBoxLayout *statusBoxLayout = new QHBoxLayout();
|
|
|
|
QToolButton *addButton = new QToolButton();
|
|
|
|
QAction *addAction = new QAction(tr("Add"));
|
|
connect(addAction, &QAction::triggered, this, &MainWindow::addKeyClicked);
|
|
|
|
addButton->setDefaultAction(addAction);
|
|
|
|
statusBoxLayout->addWidget(addButton);
|
|
statusBoxLayout->addSpacerItem(new QSpacerItem(20, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
|
|
|
|
m_statusLabel = new QLabel();
|
|
|
|
statusBoxLayout->addWidget(m_statusLabel);
|
|
|
|
vbox->addSpacerItem(new QSpacerItem(20, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
|
|
vbox->addLayout(statusBoxLayout);
|
|
|
|
m_timer->start(TIMER_INTERVAL_MS);
|
|
}
|
|
|
|
void MainWindow::onAddKeyClicked() {
|
|
// get name and key for this account and save to settings
|
|
QString name = QInputDialog::getText(this, tr("Add Key"), tr("Please enter a name for this account:"));
|
|
|
|
if(name.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Error"), tr("No name entered."));
|
|
return;
|
|
}
|
|
|
|
QString key = QInputDialog::getText(this, tr("Add Key"), tr("Please enter the TOTP key for this account:"));
|
|
|
|
// sanitize the key before saving (remove any spaces or invalid characters)
|
|
key = key.replace(QRegularExpression(R"([^a-zA-Z2-7])"), "");
|
|
key = key.toUpper();
|
|
|
|
if(key.isEmpty()) {
|
|
QMessageBox::critical(this, tr("Error"), tr("No key entered."));
|
|
return;
|
|
}
|
|
|
|
saveNewKey(name, key);
|
|
|
|
// a bit lazy and heavy-handed, but works for now
|
|
rebuildLayout();
|
|
}
|
|
|
|
void MainWindow::saveNewKey(QString name, QString key) {
|
|
// newly added key goes into our ini file
|
|
m_settings.setValue(QString(GROUP_NAME) + "/" + name, key);
|
|
m_settings.sync();
|
|
}
|
|
|
|
QString MainWindow::generateCode(QString key) {
|
|
// where the magic happens
|
|
uint32_t p = 0;
|
|
CppTotp::Bytes::ByteString qui;
|
|
|
|
try {
|
|
std::string ckey = key.toStdString();
|
|
qui = CppTotp::Bytes::fromUnpaddedBase32(ckey);
|
|
} catch(const std::invalid_argument&) {
|
|
QMessageBox::critical(0, tr("Error"), tr("Invalid key encountered, exiting."));
|
|
qApp->quit();
|
|
return QString();
|
|
}
|
|
|
|
p = CppTotp::totp(qui, time(NULL), 0, 30, 6);
|
|
|
|
// return only the first six digits, as a string, with a space in the middle
|
|
return QString::number(p).left(6).replace(QRegularExpression(R"((\d{3})(\d{3}))"), R"(\1 \2)");
|
|
}
|
|
|