Initial code for OTP client
It uses the oath-toolkit[1] provided library liboath to generate the 2FA codes, both TOTP and HOTP based. Currently it is largely untested. From initial rough testing it seems that auto-refreshing of code is not working. Also button to refresh token for HOTP is also dummy at moment. Some todo items include, - Verify the generated oath code is correct - Make refreshing token work - QR code scanning - Backup and Restore of accounts - Clipboard support to automatically copy code. - Encrypted storage of the secret token This code is largely based on the authenticator-ng[2] application by the Rodney Dawes and Michael Zanetti for the Ubuntu Touch. [1] https://www.nongnu.org/oath-toolkit/ [2] https://github.com/dobey/authenticator-ngmaster
commit
8819d205f9
|
@ -0,0 +1 @@
|
|||
.flatpak-builder/*
|
|
@ -0,0 +1,47 @@
|
|||
project(otpclient)
|
||||
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
set(KF5_MIN_VERSION "5.18.0")
|
||||
set(QT_MIN_VERSION "5.5.0")
|
||||
|
||||
################# Disallow in-source build #################
|
||||
|
||||
if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
|
||||
message(FATAL_ERROR "This application requires an out of source build. Please create a separate build directory.")
|
||||
endif()
|
||||
|
||||
include(FeatureSummary)
|
||||
|
||||
################# set KDE specific information #################
|
||||
|
||||
find_package(ECM 0.0.8 REQUIRED NO_MODULE)
|
||||
|
||||
# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} "${CMAKE_SOURCE_DIR}/cmake/")
|
||||
|
||||
include(ECMSetupVersion)
|
||||
include(ECMGenerateHeaders)
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(ECMPoQmTools)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
################# Find dependencies #################
|
||||
|
||||
find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS Core Quick Test Gui Svg QuickControls2)
|
||||
find_package(LibOath REQUIRED)
|
||||
find_package(KF5Kirigami2 ${KF5_MIN_VERSION})
|
||||
|
||||
################# Enable C++11 features for clang and gcc #################
|
||||
|
||||
if(UNIX)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++0x")
|
||||
endif()
|
||||
|
||||
################# build and install #################
|
||||
add_subdirectory(src)
|
||||
|
||||
install(PROGRAMS org.kde.otpclient.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||
install(FILES org.kde.otpclient.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||
|
||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
|
@ -0,0 +1,14 @@
|
|||
# OTP client
|
||||
|
||||
It uses the [oath-toolkit](https://www.nongnu.org/oath-toolkit/) provided library liboath to generate the 2FA codes, both TOTP and HOTP based. Currently it is largely untested. From initial rough testing it seems that auto-refreshing of code is not working. Also button to refresh token for HOTP is also dummy at moment.
|
||||
|
||||
Some todo items include,
|
||||
|
||||
- Verify the generated oath code is correct
|
||||
- Make refreshing token work
|
||||
- QR code scanning
|
||||
- Backup and Restore of accounts
|
||||
- Clipboard support to automatically copy code.
|
||||
- Encrypted storage of the secret token
|
||||
|
||||
This code is largely based on the [authenticator-ng](https://github.com/dobey/authenticator-ng) application by the Rodney Dawes and Michael Zanetti for the Ubuntu Touch.
|
|
@ -0,0 +1,83 @@
|
|||
#.rst:
|
||||
# FindLibOath
|
||||
# ---------
|
||||
#
|
||||
# Try to locate the liboath library.
|
||||
# If found, this will define the following variables:
|
||||
#
|
||||
# ``LIBOATH_FOUND``
|
||||
# True if the LibOath library is available
|
||||
# ``LIBOATH_INCLUDE_DIRS``
|
||||
# The LibOath include directories
|
||||
# ``LIBOATH_LIBRARIES``
|
||||
# The LibOath libraries for linking
|
||||
# ``LIBOATH_INCLUDE_DIR``
|
||||
# Deprecated, use ``LIBOATH_INCLUDE_DIRS``
|
||||
# ``LIBOATH_LIBRARY``
|
||||
# Deprecated, use ``LIBOATH_LIBRARIES``
|
||||
#
|
||||
# If ``LIBOATH_FOUND`` is TRUE, it will also define the following
|
||||
# imported target:
|
||||
#
|
||||
# ``LIBOATH::LIBOATH``
|
||||
# The LIBOATH library
|
||||
#
|
||||
#=============================================================================
|
||||
# Copyright (c) 2019 Bhushan Shah, <bshah@kde.org>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. The name of the author may not be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#=============================================================================
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_LIBOATH QUIET liboath)
|
||||
|
||||
find_path(LIBOATH_INCLUDE_DIRS
|
||||
NAMES oath.h
|
||||
HINTS ${PC_LIBOATH_INCLUDEDIR}
|
||||
PATH_SUFFIXES liboath)
|
||||
|
||||
find_library(LIBOATH_LIBRARIES
|
||||
NAMES oath
|
||||
HINTS ${PC_LIBOATH_LIBDIR})
|
||||
|
||||
set(LIBOATH_INCLUDE_DIR "${LIBOATH_INCLUDE_DIRS}")
|
||||
set(LIBOATH_LIBRARY "${LIBOATH_LIBRARIES}")
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBOATH DEFAULT_MSG LIBOATH_LIBRARIES LIBOATH_INCLUDE_DIRS)
|
||||
|
||||
if(LIBOATH_FOUND AND NOT TARGET LIBOATH::LIBOATH)
|
||||
add_library(LIBOATH::LIBOATH UNKNOWN IMPORTED)
|
||||
set_target_properties(LIBOATH::LIBOATH PROPERTIES
|
||||
IMPORTED_LOCATION "${LIBOATH_LIBRARIES}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${LIBOATH_INCLUDE_DIR}")
|
||||
endif()
|
||||
|
||||
mark_as_advanced(LIBOATH_INCLUDE_DIRS LIBOATH_INCLUDE_DIR
|
||||
LIBOATH_LIBRARIES LIBOATH_LIBRARY)
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(LIBOATH PROPERTIES
|
||||
URL "http://www.nongnu.org/oath-toolkit/"
|
||||
DESCRIPTION "Library for Open AuTHentication (OATH) HOTP etc support.")
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>org.kde.otpclient</id>
|
||||
<name>Kirigami Example Application</name>
|
||||
<summary>A short summary describing what this software is about</summary>
|
||||
<metadata_license>A permissive license for this metadata, e.g. "FSFAP"</metadata_license>
|
||||
<project_license>The license of this software as SPDX string, e.g. "GPL-3+"</project_license>
|
||||
<developer_name>The software vendor name, e.g. "ACME Corporation"</developer_name>
|
||||
<description>
|
||||
<p>Multiple paragraphs of long description, describing this software component.</p>
|
||||
<p>You can also use ordered and unordered lists:</p>
|
||||
<ul>
|
||||
<li>Feature 1</li>
|
||||
<li>Feature 2</li>
|
||||
</ul>
|
||||
<p>Keep in mind to XML-escape characters, and that this is not HTML markup.</p>
|
||||
</description>
|
||||
</component>
|
|
@ -0,0 +1,9 @@
|
|||
[Desktop Entry]
|
||||
Name=OTP client
|
||||
Comment=My first Plasma Mobile App
|
||||
Version=1.0
|
||||
Exec=org.kde.otpclient
|
||||
Icon=applications-development
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Categories=Qt;KDE;
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"id": "org.kde.otpclient",
|
||||
"runtime": "org.kde.Platform",
|
||||
"runtime-version": "5.12",
|
||||
"sdk": "org.kde.Sdk",
|
||||
"command": "org.kde.otpclient",
|
||||
"tags": ["nightly"],
|
||||
"desktop-file-name-suffix": " (Nightly)",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--socket=x11",
|
||||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
"--filesystem=home",
|
||||
"--talk-name=org.freedesktop.Notifications"
|
||||
],
|
||||
"separate-locales": false,
|
||||
|
||||
"modules": [
|
||||
{
|
||||
"name": "xmlsec1",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://www.aleksey.com/xmlsec/download/xmlsec1-1.2.27.tar.gz",
|
||||
"sha256": "97d756bad8e92588e6997d2227797eaa900d05e34a426829b149f65d87118eb6"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "oath-toolkit",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "http://download.savannah.nongnu.org/releases/oath-toolkit/oath-toolkit-2.6.2.tar.gz",
|
||||
"sha256": "b03446fa4b549af5ebe4d35d7aba51163442d255660558cd861ebce536824aa0"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "flatpak/2fffce2a471f74a585939c84cce16ef3015e5d3d.diff",
|
||||
"sha256": "4093d69a22af60fac339fcee22ff29c3b8418b76bc1286e5226505af884e0c21"
|
||||
},
|
||||
{
|
||||
"type" : "shell",
|
||||
"commands" : [ "autoreconf -vfi" ]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "org.kde.otpclient",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"builddir": true,
|
||||
"sources": [ { "type": "dir", "path": ".", "skip": [".git"] } ]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
set(otpclient_SRCS
|
||||
main.cpp
|
||||
accountmodel.cpp
|
||||
account.cpp
|
||||
)
|
||||
|
||||
qt5_add_resources(RESOURCES resources.qrc)
|
||||
add_executable(org.kde.otpclient ${otpclient_SRCS} ${RESOURCES})
|
||||
target_link_libraries(org.kde.otpclient Qt5::Core Qt5::Qml Qt5::Quick Qt5::Svg ${LIBOATH_LIBRARIES})
|
||||
install(TARGETS org.kde.otpclient ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
|
|
@ -0,0 +1,243 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
|
||||
#ifndef SIZE_MAX
|
||||
#define SIZE_MAX UINT_MAX
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
#include <liboath/oath.h>
|
||||
}
|
||||
|
||||
Account::Account(const QUuid &id, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_id(id),
|
||||
m_counter(0),
|
||||
m_timeStep(30),
|
||||
m_pinLength(6)
|
||||
{
|
||||
m_totpTimer.setSingleShot(true);
|
||||
connect(&m_totpTimer, SIGNAL(timeout()), SLOT(generate()));
|
||||
}
|
||||
|
||||
QUuid Account::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString Account::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
void Account::setName(const QString &name)
|
||||
{
|
||||
if (m_name != name) {
|
||||
m_name = name;
|
||||
emit nameChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Account::Type Account::type() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
|
||||
void Account::setType(Account::Type type)
|
||||
{
|
||||
if (m_type != type) {
|
||||
m_type = type;
|
||||
// qDebug() << "setting type" << type;
|
||||
emit typeChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
QString Account::secret() const
|
||||
{
|
||||
return m_secret;
|
||||
}
|
||||
|
||||
void Account::setSecret(const QString &secret)
|
||||
{
|
||||
if (m_secret != secret) {
|
||||
m_secret = secret;
|
||||
emit secretChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
quint64 Account::counter() const
|
||||
{
|
||||
return m_counter;
|
||||
}
|
||||
|
||||
void Account::setCounter(quint64 counter)
|
||||
{
|
||||
if (m_counter != counter) {
|
||||
m_counter = counter;
|
||||
emit counterChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
int Account::timeStep() const
|
||||
{
|
||||
return m_timeStep;
|
||||
}
|
||||
|
||||
void Account::setTimeStep(int timeStep)
|
||||
{
|
||||
if (m_timeStep != timeStep) {
|
||||
m_timeStep = timeStep;
|
||||
emit timeStepChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
int Account::pinLength() const
|
||||
{
|
||||
return m_pinLength;
|
||||
}
|
||||
|
||||
void Account::setPinLength(int pinLength)
|
||||
{
|
||||
if (m_pinLength != pinLength) {
|
||||
m_pinLength = pinLength;
|
||||
emit pinLengthChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
QString Account::otp() const
|
||||
{
|
||||
return m_otp;
|
||||
}
|
||||
|
||||
qint64 Account::msecsToNext() const
|
||||
{
|
||||
if (m_timeStep <= 0) {
|
||||
return 0;
|
||||
}
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
qint64 msecsSinceLast = now % (m_timeStep * 1000);
|
||||
qint64 msecsToNext = (m_timeStep * 1000) - msecsSinceLast;
|
||||
return msecsToNext;
|
||||
}
|
||||
|
||||
void Account::next()
|
||||
{
|
||||
m_counter++;
|
||||
// qDebug() << "emitting changed";
|
||||
emit counterChanged();
|
||||
generate();
|
||||
}
|
||||
|
||||
void Account::generate()
|
||||
{
|
||||
if (m_secret.isEmpty()) {
|
||||
// qWarning() << "No secret set. Cannot generate otp.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pinLength <= 0) {
|
||||
// qWarning() << "Pin length is" << m_pinLength << ". Cannot generate otp.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_type == TypeTOTP && m_timeStep <= 0) {
|
||||
// qWarning() << "Time step is 0. Cannot generate totp";
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << "generating for account" << m_name;
|
||||
QByteArray hexSecret = fromBase32(m_secret.toLatin1());
|
||||
// qDebug() << "hexSecret" << hexSecret;
|
||||
char code[m_pinLength];
|
||||
if (m_type == TypeHOTP) {
|
||||
oath_hotp_generate(hexSecret.data(), hexSecret.length(), m_counter, m_pinLength, false, OATH_HOTP_DYNAMIC_TRUNCATION, code);
|
||||
} else {
|
||||
oath_totp_generate(hexSecret.data(), hexSecret.length(), QDateTime::currentDateTime().toTime_t(), m_timeStep, 0, m_pinLength, code);
|
||||
}
|
||||
|
||||
m_otp = QLatin1String(code);
|
||||
// qDebug() << "Generating secret" << m_name << m_secret << m_counter << m_pinLength << m_otp << m_timeStep;
|
||||
emit otpChanged();
|
||||
|
||||
if (m_type == TypeTOTP) {
|
||||
|
||||
// QTimer tends to be a wee bit too early...
|
||||
// let's just add half a sec to make sure we end up in
|
||||
// the current time slot and avoid restarting timers in the ui
|
||||
m_totpTimer.setInterval(msecsToNext() + 500);
|
||||
// qDebug() << "restarting timer for" << m_name << m_totpTimer.interval() << msecsToNext << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
m_totpTimer.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QByteArray Account::fromBase32(const QByteArray &input)
|
||||
{
|
||||
int buffer = 0;
|
||||
int bitsLeft = 0;
|
||||
int count = 0;
|
||||
|
||||
QByteArray result;
|
||||
|
||||
for (int i = 0; i < input.length(); ++i) {
|
||||
|
||||
char ch = input.at(i);
|
||||
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-') {
|
||||
continue;
|
||||
}
|
||||
buffer <<= 5;
|
||||
|
||||
if (ch == '0') {
|
||||
ch = 'O';
|
||||
} else if (ch == '1') {
|
||||
ch = 'L';
|
||||
} else if (ch == '8') {
|
||||
ch = 'B';
|
||||
}
|
||||
|
||||
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
|
||||
ch = (ch & 0x1F) - 1;
|
||||
} else if (ch >= '2' && ch <= '7') {
|
||||
ch -= '2' - 26;
|
||||
} else {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
buffer |= ch;
|
||||
bitsLeft += 5;
|
||||
if (bitsLeft >= 8) {
|
||||
result[count++] = buffer >> (bitsLeft - 8);
|
||||
bitsLeft -= 8;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef ACCOUNT_H
|
||||
#define ACCOUNT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUuid>
|
||||
#include <QTimer>
|
||||
|
||||
class Account : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_ENUMS(Type)
|
||||
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
|
||||
Q_PROPERTY(Type type READ type WRITE setType NOTIFY typeChanged)
|
||||
Q_PROPERTY(QString secret READ secret WRITE setSecret NOTIFY secretChanged)
|
||||
Q_PROPERTY(quint64 counter READ counter WRITE setCounter NOTIFY counterChanged)
|
||||
Q_PROPERTY(int timeStep READ timeStep WRITE setTimeStep NOTIFY timeStepChanged)
|
||||
Q_PROPERTY(int pinLength READ pinLength WRITE setPinLength NOTIFY pinLengthChanged)
|
||||
Q_PROPERTY(QString otp READ otp NOTIFY otpChanged)
|
||||
public:
|
||||
enum Type {
|
||||
TypeHOTP,
|
||||
TypeTOTP
|
||||
};
|
||||
|
||||
explicit Account(const QUuid &id, QObject *parent = 0);
|
||||
|
||||
QUuid id() const;
|
||||
|
||||
QString name() const;
|
||||
void setName(const QString &name);
|
||||
|
||||
Type type() const;
|
||||
void setType(Type type);
|
||||
|
||||
QString secret() const;
|
||||
void setSecret(const QString &secret);
|
||||
|
||||
quint64 counter() const;
|
||||
void setCounter(quint64 counter);
|
||||
|
||||
int timeStep() const;
|
||||
void setTimeStep(int timeStep);
|
||||
|
||||
int pinLength() const;
|
||||
void setPinLength(int pinLength);
|
||||
|
||||
QString otp() const;
|
||||
|
||||
Q_INVOKABLE qint64 msecsToNext() const;
|
||||
|
||||
signals:
|
||||
void nameChanged();
|
||||
void typeChanged();
|
||||
void secretChanged();
|
||||
void counterChanged();
|
||||
void timeStepChanged();
|
||||
void pinLengthChanged();
|
||||
void otpChanged();
|
||||
|
||||
public slots:
|
||||
void generate();
|
||||
void next();
|
||||
|
||||
private:
|
||||
|
||||
static QByteArray fromBase32(const QByteArray &input);
|
||||
|
||||
private:
|
||||
QUuid m_id;
|
||||
QString m_name;
|
||||
Type m_type;
|
||||
QString m_secret;
|
||||
quint64 m_counter;
|
||||
int m_timeStep;
|
||||
int m_pinLength;
|
||||
QString m_otp;
|
||||
QTimer m_totpTimer;
|
||||
};
|
||||
|
||||
#endif // ACCOUNT_H
|
|
@ -0,0 +1,196 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "accountmodel.h"
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
//#include <QDebug>
|
||||
|
||||
AccountModel::AccountModel(QObject *parent) :
|
||||
QAbstractListModel(parent)
|
||||
{
|
||||
QSettings settings("org.kde.otpclient", "otpclient");
|
||||
// qDebug() << "loading settings file:" << settings.fileName();
|
||||
foreach(const QString & group, settings.childGroups()) {
|
||||
// qDebug() << "found group" << group << QUuid(group).toString();
|
||||
|
||||
QUuid id = QUuid(group);
|
||||
bool migrateAccount = false;
|
||||
if (id.isNull()) {
|
||||
migrateAccount = true;
|
||||
id = QUuid::createUuid();
|
||||
}
|
||||
|
||||
settings.beginGroup(group);
|
||||
Account *account = new Account(id, this);
|
||||
account->setName(settings.value("account").toString());
|
||||
account->setType(settings.value("type", "hotp").toString() == "totp" ? Account::TypeTOTP : Account::TypeHOTP);
|
||||
account->setSecret(settings.value("secret").toString());
|
||||
account->setCounter(settings.value("counter").toInt());
|
||||
account->setTimeStep(settings.value("timeStep").toInt());
|
||||
account->setPinLength(settings.value("pinLength").toInt());
|
||||
|
||||
connect(account, SIGNAL(nameChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(typeChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(secretChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(counterChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(timeStepChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(pinLengthChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(otpChanged()), SLOT(accountChanged()));
|
||||
|
||||
m_accounts.append(account);
|
||||
|
||||
if (migrateAccount) {
|
||||
settings.remove("");
|
||||
storeAccount(account);
|
||||
}
|
||||
settings.endGroup();
|
||||
}
|
||||
}
|
||||
|
||||
int AccountModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
QVariant AccountModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case RoleName:
|
||||
return m_accounts.at(index.row())->name();
|
||||
case RoleType:
|
||||
return m_accounts.at(index.row())->type();
|
||||
case RoleSecret:
|
||||
return m_accounts.at(index.row())->secret();
|
||||
case RoleCounter:
|
||||
return m_accounts.at(index.row())->counter();
|
||||
case RoleTimeStep:
|
||||
return m_accounts.at(index.row())->timeStep();
|
||||
case RolePinLength:
|
||||
return m_accounts.at(index.row())->pinLength();
|
||||
case RoleOtp:
|
||||
return m_accounts.at(index.row())->otp();
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Account *AccountModel::get(int index) const
|
||||
{
|
||||
if (index > -1 && m_accounts.count() > index) {
|
||||
return m_accounts.at(index);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Account *AccountModel::createAccount()
|
||||
{
|
||||
Account *account = new Account(QUuid::createUuid(), this);
|
||||
beginInsertRows(QModelIndex(), m_accounts.count(), m_accounts.count());
|
||||
m_accounts.append(account);
|
||||
connect(account, SIGNAL(nameChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(typeChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(secretChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(counterChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(pinLengthChanged()), SLOT(accountChanged()));
|
||||
connect(account, SIGNAL(otpChanged()), SLOT(accountChanged()));
|
||||
|
||||
storeAccount(account);
|
||||
|
||||
endInsertRows();
|
||||
return account;
|
||||
}
|
||||
|
||||
void AccountModel::deleteAccount(int index)
|
||||
{
|
||||
// qDebug() << "starting deleteAccount" << index << m_accounts.count();
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
|
||||
Account *account = m_accounts.takeAt(index);
|
||||
// qDebug() << "got account" << account;
|
||||
QSettings settings("org.kde.otpclient", "otpclient");
|
||||
settings.beginGroup(account->id().toString());
|
||||
settings.remove("");
|
||||
settings.endGroup();
|
||||
|
||||
// qDebug() << "removed from settings";
|
||||
account->deleteLater();
|
||||
|
||||
endRemoveRows();
|
||||
// qDebug() << "done with deleteAccount";
|
||||
}
|
||||
|
||||
void AccountModel::deleteAccount(Account *account)
|
||||
{
|
||||
int index = m_accounts.indexOf(account);
|
||||
deleteAccount(index);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(RoleName, "name");
|
||||
roles.insert(RoleType, "type");
|
||||
roles.insert(RoleSecret, "secret");
|
||||
roles.insert(RoleCounter, "counter");
|
||||
roles.insert(RoleTimeStep, "timeStep");
|
||||
roles.insert(RolePinLength, "pinLength");
|
||||
roles.insert(RoleOtp, "otp");
|
||||
return roles;
|
||||
}
|
||||
|
||||
void AccountModel::generateNext(int account)
|
||||
{
|
||||
m_accounts.at(account)->next();
|
||||
emit dataChanged(index(account), index(account), QVector<int>() << RoleCounter << RoleOtp);
|
||||
}
|
||||
|
||||
void AccountModel::refresh()
|
||||
{
|
||||
emit beginResetModel();
|
||||
emit endResetModel();
|
||||
}
|
||||
|
||||
void AccountModel::accountChanged()
|
||||
{
|
||||
Account *account = qobject_cast<Account*>(sender());
|
||||
storeAccount(account);
|
||||
|
||||
// qDebug() << "account changed";
|
||||
int accountIndex = m_accounts.indexOf(account);
|
||||
emit dataChanged(index(accountIndex), index(accountIndex));
|
||||
}
|
||||
|
||||
void AccountModel::storeAccount(Account *account)
|
||||
{
|
||||
QSettings settings("org.kde.otpclient", "otpclient");
|
||||
settings.beginGroup(account->id().toString());
|
||||
settings.setValue("account", account->name());
|
||||
settings.setValue("type", account->type() == Account::TypeTOTP ? "totp" : "hotp");
|
||||
settings.setValue("secret", account->secret());
|
||||
settings.setValue("counter", account->counter());
|
||||
settings.setValue("timeStep", account->timeStep());
|
||||
settings.setValue("pinLength", account->pinLength());
|
||||
settings.endGroup();
|
||||
// qDebug() << "saved to" << settings.fileName();
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef ACCOUNTMODEL_H
|
||||
#define ACCOUNTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class Account;
|
||||
|
||||
class AccountModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Roles {
|
||||
RoleName,
|
||||
RoleType,
|
||||
RoleSecret,
|
||||
RoleCounter,
|
||||
RoleTimeStep,
|
||||
RolePinLength,
|
||||
RoleOtp
|
||||
};
|
||||
|
||||
explicit AccountModel(QObject *parent = 0);
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE Account *get(int index) const;
|
||||
Q_INVOKABLE Account *createAccount();
|
||||
Q_INVOKABLE void deleteAccount(int index);
|
||||
Q_INVOKABLE void deleteAccount(Account *account);
|
||||
|
||||
|
||||
public slots:
|
||||
void generateNext(int account);
|
||||
void refresh();
|
||||
|
||||
private slots:
|
||||
void accountChanged();
|
||||
void storeAccount(Account *account);
|
||||
|
||||
private:
|
||||
QList<Account*> m_accounts;
|
||||
};
|
||||
|
||||
#endif // ACCOUNTMODEL_H
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright 2019 Bhushan Shah <bshah@kde.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3 of
|
||||
* the License or any later version accepted by the membership of
|
||||
* KDE e.V. (or its successor approved by the membership of KDE
|
||||
* e.V.), which shall act as a proxy defined in Section 14 of
|
||||
* version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import OAth 1.0
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls 2.0 as Controls
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
title: "OTP Client"
|
||||
|
||||
pageStack.initialPage: mainPageComponent
|
||||
|
||||
AccountModel {
|
||||
id: accounts
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mainPageComponent
|
||||
Kirigami.ScrollablePage {
|
||||
title: "OTP"
|
||||
actions.main: Kirigami.Action {
|
||||
text: "Add"
|
||||
iconName: "list-add"
|
||||
onTriggered: {
|
||||
pageStack.push(addPageComponent);
|
||||
}
|
||||
}
|
||||
Controls.Label {
|
||||
text: "No account set up. Use the add button to add accounts."
|
||||
visible: view.count == 0
|
||||
}
|
||||
Kirigami.CardsListView {
|
||||
id: view
|
||||
model: accounts
|
||||
delegate: Kirigami.AbstractCard {
|
||||
contentItem: Item {
|
||||
implicitWidth: delegateLayout.implicitWidth
|
||||
implicitHeight: delegateLayout.implicitHeight
|
||||
GridLayout {
|
||||
id: delegateLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
//IMPORTANT: never put the bottom margin
|
||||
}
|
||||
rowSpacing: Kirigami.Units.largeSpacing
|
||||
columnSpacing: Kirigami.Units.largeSpacing
|
||||
columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2
|
||||
ColumnLayout {
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
text: name
|
||||
}
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: otp
|
||||
}
|
||||
}
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignRight|Qt.AlignVCenter
|
||||
Layout.columnSpan: 2
|
||||
text: "Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: addPageComponent
|
||||
Kirigami.Page {
|
||||
title: "Add new OTP"
|
||||
actions.main: Kirigami.Action {
|
||||
text: "Add"
|
||||
iconName: "answer-correct"
|
||||
onTriggered: {
|
||||
var newAccount = accounts.createAccount();
|
||||
newAccount.name = accountName.text;
|
||||
newAccount.type = totpRadio.checked ? Account.TypeTOTP : Account.TypeHOTP
|
||||
newAccount.secret = accountSecret.text
|
||||
newAccount.counter = parseInt(counterField.text)
|
||||
newAccount.timeStep = parseInt(timerField.text)
|
||||
newAccount.pinLength = parseInt(pinLengthField.text)
|
||||
pageStack.pop();
|
||||
}
|
||||
}
|
||||
Kirigami.FormLayout {
|
||||
id: layout
|
||||
Controls.TextField {
|
||||
id: accountName
|
||||
Kirigami.FormData.label: "Account Name:"
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.rowSpan: 2
|
||||
Kirigami.FormData.label: "Account Type:"
|
||||
Kirigami.FormData.buddyFor: totpRadio
|
||||
Controls.RadioButton {
|
||||
id: totpRadio
|
||||
checked: true
|
||||
text: "Time-based OTP"
|
||||
}
|
||||
Controls.RadioButton {
|
||||
id: hotpRadio
|
||||
text: "Hash-based OTP"
|
||||
}
|
||||
}
|
||||
Controls.TextField {
|
||||
id: accountSecret
|
||||
Kirigami.FormData.label: "Secret key:"
|
||||
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
|
||||
}
|
||||
Controls.TextField {
|
||||
id: timerField
|
||||
Kirigami.FormData.label: "Timer:"
|
||||
enabled: totpRadio.checked
|
||||
text: "30"
|
||||
inputMask: "0009"
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
}
|
||||
Controls.TextField {
|
||||
id: counterField
|
||||
Kirigami.FormData.label: "Counter:"
|
||||
enabled: hotpRadio.checked
|
||||
inputMask: "0009"
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
}
|
||||
Controls.TextField {
|
||||
id: pinLengthField
|
||||
Kirigami.FormData.label: "Pin length:"
|
||||
text: "6"
|
||||
inputMask: "0D"
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2019 Bhushan Shah <bshah@kde.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3 of
|
||||
* the License or any later version accepted by the membership of
|
||||
* KDE e.V. (or its successor approved by the membership of KDE
|
||||
* e.V.), which shall act as a proxy defined in Section 14 of
|
||||
* version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QtQml>
|
||||
#include <QUrl>
|
||||
|
||||
#include "accountmodel.h"
|
||||
#include "account.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QApplication app(argc, argv);
|
||||
QCoreApplication::setOrganizationName("KDE");
|
||||
QCoreApplication::setOrganizationDomain("kde.org");
|
||||
QCoreApplication::setApplicationName("otpclient");
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
qmlRegisterType<AccountModel>("OAth", 1, 0, "AccountModel");
|
||||
qmlRegisterUncreatableType<Account>("OAth", 1, 0, "Account", "Use AccountModel::createAccount() to create a new account");
|
||||
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
|
||||
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
int ret = app.exec();
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file alias="main.qml">contents/ui/main.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
Loading…
Reference in New Issue