Browse Source

initial release

master 1.0
Brad Parker 4 years ago
parent
commit
85bb65d174
  1. 9
      .gitignore
  2. 33
      README.md
  3. 7
      libcppotp/COPYING
  4. 1
      libcppotp/README
  5. 341
      libcppotp/bytes.cpp
  6. 70
      libcppotp/bytes.h
  7. 104
      libcppotp/otp.cpp
  8. 37
      libcppotp/otp.h
  9. 224
      libcppotp/sha1.cpp
  10. 34
      libcppotp/sha1.h
  11. 19
      main.cpp
  12. 187
      mainwindow.cpp
  13. 25
      mainwindow.h
  14. 6
      twofap.pro

9
.gitignore

@ -0,0 +1,9 @@
twofap.ini
*.swp
*~
*.pro.user
*.exe
*.qmake.stash
debug/
release/
Makefile*

33
README.md

@ -0,0 +1,33 @@
TWOFAP - Two Factor Authentication Program
# Description
A simple, cross-platform, Qt-based TOTP (RFC 6238) generator. I was not able to find a similar existing program online that was still maintained and easy to compile so I decided to make my own.
The program will generate time-based one-time passwords as used by many popular websites and services such as Google, Microsoft, Twitter, Discord etc.
This program does not phone home, it does not track you, do anything intentionally malicious, or use any network resources of any kind. It works completely offline.
The license for this program is Apache 2.0. It comes with no warranty of any kind. Please do not blame me if the software eats your system, feeds your dog, destroys your disk or makes your cornflakes soggy.
The development is an early work-in-progress but should be stable enough for daily use.
Keys are currently stored in plain text in a `twofap.ini` file in the current working directory (not necessarily the same directory as the binary itself). It is up to the user to make sure this file does not fall into the wrong hands. Later I might add an option to encrypt the file or something, but right now it is not a priority for me... 2FA is troublesome enough, I don't need 3FA.
The actual handling of the TOTP keys and the code generation itself is done using the public domain "libcppotp" library from Ondřej Hošek at https://github.com/RavuAlHemio/cpptotp. Thanks very much for that.
# Compiling
You will need Qt5 and a C++11 compiler toolchain. There are no other external dependencies.
First, fetch the repo with:
`git clone https://github.com/bparker06/twofap` or use your favorite Git GUI and provide the same URL to clone.
To build from the command-line:
Just run `qmake && make`.
To build from an IDE:
If you're using Qt Creator or another IDE that supports Qt (like Visual Studio with the Qt add-in), just open the .pro file and click Build.

7
libcppotp/COPYING

@ -0,0 +1,7 @@
PUBLIC DOMAIN DEDICATION
The author of this work has dedicated the work to the public domain by waiving
all of their rights to the work worldwide under copyright law, including all
related and neighboring rights, to the extent allowed by law.
http://creativecommons.org/publicdomain/zero/1.0/

1
libcppotp/README

@ -0,0 +1 @@
Taken from https://github.com/RavuAlHemio/cpptotp

341
libcppotp/bytes.cpp

@ -0,0 +1,341 @@
/**
* @file bytes.cpp
*
* @brief Byte-related operations.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "bytes.h"
#include <iostream>
#include <stdexcept>
#include <cassert>
#include <cstdlib>
namespace CppTotp
{
namespace Bytes
{
void clearByteString(ByteString * bstr)
{
volatile Byte * bs = const_cast<volatile Byte *>(bstr->data());
for (size_t i = 0; i < bstr->size(); ++i)
{
bs[i] = Byte(0);
}
}
void swizzleByteStrings(ByteString * target, ByteString * source)
{
clearByteString(target);
target->assign(*source);
clearByteString(source);
}
static char nibbleToLCHex(uint8_t nib)
{
if (nib < 0xa)
{
return static_cast<char>(nib + '0');
}
else if (nib < 0x10)
{
return static_cast<char>((nib - 10) + 'a');
}
else
{
assert(0 && "not actually a nibble");
return '\0';
}
}
static uint8_t hexToNibble(char c)
{
if (c >= '0' && c <= '9')
{
return static_cast<uint8_t>(c - '0');
}
else if (c >= 'A' && c <= 'F')
{
return static_cast<uint8_t>(c - 'A' + 10);
}
else if (c >= 'a' && c <= 'f')
{
return static_cast<uint8_t>(c - 'a' + 10);
}
else
{
assert(0 && "not actually a hex digit");
return 0xff;
}
}
std::string toHexString(const ByteString & bstr)
{
std::string ret;
for (Byte b : bstr)
{
ret.push_back(nibbleToLCHex((b >> 4) & 0x0F));
ret.push_back(nibbleToLCHex((b >> 0) & 0x0F));
}
return ret;
}
ByteString fromHexStringSkipUnknown(const std::string & str)
{
std::string hstr;
for (char c : str)
{
if (
(c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f')
)
{
hstr.push_back(c);
}
// ignore otherwise
}
if (hstr.size() % 2 != 0)
{
throw std::invalid_argument("hex string (unknown characters ignored) length not divisible by 2");
}
ByteString ret;
for (size_t i = 0; i < hstr.size(); i += 2)
{
uint8_t top = hexToNibble(hstr[i+0]);
uint8_t btm = hexToNibble(hstr[i+1]);
ret.push_back((top << 4) | btm);
}
return ret;
}
Bytes::ByteString u32beToByteString(uint32_t num)
{
Bytes::ByteString ret;
ret.push_back((num >> 24) & 0xFF);
ret.push_back((num >> 16) & 0xFF);
ret.push_back((num >> 8) & 0xFF);
ret.push_back((num >> 0) & 0xFF);
return ret;
}
Bytes::ByteString u64beToByteString(uint64_t num)
{
Bytes::ByteString left = u32beToByteString((num >> 32) & 0xFFFFFFFF);
Bytes::ByteString right = u32beToByteString((num >> 0) & 0xFFFFFFFF);
return left + right;
}
static ByteString b32ChunkToBytes(const std::string & str)
{
ByteString ret;
uint64_t whole = 0x00;
size_t padcount = 0;
size_t finalcount;
if (str.length() != 8)
{
throw std::invalid_argument("incorrect length of base32 chunk");
}
size_t i;
for (i = 0; i < 8; ++i)
{
char c = str[i];
uint64_t bits;
if (c == '=')
{
bits = 0;
++padcount;
}
else if (padcount > 0)
{
throw std::invalid_argument("padding character followed by non-padding character");
}
else if (c >= 'A' && c <= 'Z')
{
bits = static_cast<Byte>(c - 'A');
}
else if (c >= '2' && c <= '7')
{
bits = static_cast<Byte>(c - '2' + 26);
}
else
{
throw std::invalid_argument("not a base32 character: " + std::string(1, c));
}
// shift into the chunk
whole |= (bits << ((7-i)*5));
}
switch (padcount)
{
case 0:
finalcount = 5;
break;
case 1:
finalcount = 4;
break;
case 3:
finalcount = 3;
break;
case 4:
finalcount = 2;
break;
case 6:
finalcount = 1;
break;
default:
throw std::invalid_argument("invalid number of padding characters");
}
for (i = 0; i < finalcount; ++i)
{
// shift out of the chunk
ret.push_back(static_cast<Byte>((whole >> ((4-i)*8)) & 0xFF));
}
return ret;
}
static inline uint64_t u64(uint8_t n)
{
return static_cast<uint64_t>(n);
}
static std::string bytesToB32Chunk(const ByteString & bs)
{
if (bs.size() < 1 || bs.size() > 5)
{
throw std::invalid_argument("need a chunk of at least 1 and at most 5 bytes");
}
uint64_t whole = 0x00;
size_t putchars = 2;
std::string ret;
// shift into the chunk
whole |= (u64(bs[0]) << 32);
if (bs.size() > 1)
{
whole |= (u64(bs[1]) << 24);
putchars += 2; // at least 4
}
if (bs.size() > 2)
{
whole |= (u64(bs[2]) << 16);
++putchars; // at least 5
}
if (bs.size() > 3)
{
whole |= (u64(bs[3]) << 8);
putchars += 2; // at least 7
}
if (bs.size() > 4)
{
whole |= u64(bs[4]);
++putchars; // at least 8
}
size_t i;
for (i = 0; i < putchars; ++i)
{
// shift out of the chunk
Byte val = (whole >> ((7-i)*5)) & 0x1F;
// map bits to base32
if (val < 26)
{
ret.push_back(static_cast<char>(val + 'A'));
}
else
{
ret.push_back(static_cast<char>(val - 26 + '2'));
}
}
// pad
for (i = putchars; i < 8; ++i)
{
ret.push_back('=');
}
return ret;
}
ByteString fromBase32(const std::string & b32str)
{
if (b32str.size() % 8 != 0)
{
throw std::invalid_argument("base32 string length not divisible by 8");
}
ByteString ret;
for (size_t i = 0; i < b32str.size(); i += 8)
{
std::string sub(b32str, i, 8);
ByteString chk = b32ChunkToBytes(sub);
ret.append(chk);
}
return ret;
}
ByteString fromUnpaddedBase32(const std::string & b32str)
{
std::string newstr = b32str;
while (newstr.size() % 8 != 0)
{
newstr.push_back('=');
}
return fromBase32(newstr);
}
std::string toBase32(const ByteString & bs)
{
std::string ret;
size_t i, j, len;
for (j = 0; j < bs.size() / 5; ++j)
{
i = j * 5;
ByteString sub(bs, i, 5);
std::string chk = bytesToB32Chunk(sub);
ret.append(chk);
}
i = j * 5;
len = bs.size() - i;
if (len > 0)
{
// block of size < 5 remains
ByteString sub(bs, i, std::string::npos);
std::string chk = bytesToB32Chunk(sub);
ret.append(chk);
}
return ret;
}
}
}

70
libcppotp/bytes.h

@ -0,0 +1,70 @@
/**
* @file bytes.h
*
* @brief Byte-related operations.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_BYTES_H__
#define __CPPTOTP_BYTES_H__
#include <string>
#include <cstdint>
namespace CppTotp
{
namespace Bytes
{
/** The type of a single byte. */
typedef uint8_t Byte;
/** The type of a byte string. */
typedef std::basic_string<Byte> ByteString;
/** Deletes the contents of a byte string. */
void clearByteString(ByteString * bstr);
/** Replaces target with source, clearing as much as possible. */
void swizzleByteStrings(ByteString * target, ByteString * source);
/** Converts a byte string into a hex string. */
std::string toHexString(const ByteString & bstr);
/** Converts an unsigned 32-bit integer into a corresponding byte string. */
ByteString u32beToByteString(uint32_t num);
/** Converts an unsigned 64-bit integer into a corresponding byte string. */
ByteString u64beToByteString(uint64_t num);
/** Converts a Base32 string into the correspoding byte string. */
ByteString fromBase32(const std::string & b32str);
/**
* Converts a potentially unpadded Base32 string into the corresponding byte
* string.
*/
ByteString fromUnpaddedBase32(const std::string & b32str);
/** Converts byte string into the corresponding Base32 string. */
std::string toBase32(const ByteString & b32str);
/** Deletes the contets of a byte string on destruction. */
class ByteStringDestructor
{
private:
/** The byte string to clear. */
ByteString * m_bs;
public:
ByteStringDestructor(ByteString * bs) : m_bs(bs) {}
~ByteStringDestructor() { clearByteString(m_bs); }
};
}
}
#endif

104
libcppotp/otp.cpp

@ -0,0 +1,104 @@
/**
* @file otp.cpp
*
* @brief Implementations of one-time-password-related functions.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "otp.h"
#include <iostream>
#include <cassert>
#include <cinttypes>
#include <cstring>
namespace CppTotp
{
Bytes::ByteString hmacSha1_64(const Bytes::ByteString & key, const Bytes::ByteString & msg)
{
return hmacSha1(key, msg, 64);
}
//uint32_t hotp(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t digitCount, HmacFunc hmacf)
uint32_t hotp(const Bytes::ByteString & key, uint64_t counter, size_t digitCount, HmacFunc hmacf)
{
Bytes::ByteString msg = Bytes::u64beToByteString(counter);
Bytes::ByteStringDestructor dmsg(&msg);
Bytes::ByteString hmac = hmacf(key, msg);
Bytes::ByteStringDestructor dhmac(&hmac);
uint32_t digits10 = 1;
for (size_t i = 0; i < digitCount; ++i)
{
digits10 *= 10;
}
// fetch the offset (from the last nibble)
uint8_t offset = hmac[hmac.size()-1] & 0x0F;
// fetch the four bytes from the offset
Bytes::ByteString fourWord = hmac.substr(offset, 4);
Bytes::ByteStringDestructor dfourWord(&fourWord);
// turn them into a 32-bit integer
uint32_t ret =
(fourWord[0] << 24) |
(fourWord[1] << 16) |
(fourWord[2] << 8) |
(fourWord[3] << 0)
;
// snip off the MSB (to alleviate signed/unsigned troubles)
// and calculate modulo digit count
return (ret & 0x7fffffff) % digits10;
}
uint32_t totp(const Bytes::ByteString & key, uint64_t timeNow, uint64_t timeStart, uint64_t timeStep, size_t digitCount, HmacFunc hmacf)
{
uint64_t timeValue = (timeNow - timeStart) / timeStep;
return hotp(key, timeValue, digitCount, hmacf);
}
}
#if TEST_OTP
int main(void)
{
using namespace CppTotp;
uint64_t start = 0;
uint64_t step = 30;
uint8_t digitsH = 6;
uint8_t digitsT = 8;
const Bytes::ByteString key = reinterpret_cast<const uint8_t *>("12345678901234567890");
std::cout
<< (hotp(key, 0, digitsH) == 755224)
<< (hotp(key, 1, digitsH) == 287082)
<< (hotp(key, 2, digitsH) == 359152)
<< (hotp(key, 3, digitsH) == 969429)
<< (hotp(key, 4, digitsH) == 338314)
<< (hotp(key, 5, digitsH) == 254676)
<< (hotp(key, 6, digitsH) == 287922)
<< (hotp(key, 7, digitsH) == 162583)
<< (hotp(key, 8, digitsH) == 399871)
<< (hotp(key, 9, digitsH) == 520489)
<< (totp(key, 59, start, step, digitsT) == 94287082)
<< (totp(key, 1111111109, start, step, digitsT) == 7081804)
<< (totp(key, 1111111111, start, step, digitsT) == 14050471)
<< (totp(key, 1234567890, start, step, digitsT) == 89005924)
<< (totp(key, 2000000000, start, step, digitsT) == 69279037)
<< (totp(key, 20000000000, start, step, digitsT) == 65353130)
<< std::endl;
const Bytes::ByteString tutestkey = reinterpret_cast<const uint8_t *>("HelloWorld");
std::cout << totp(tutestkey, time(NULL), 0, 30, 6) << std::endl;
return 0;
}
#endif

37
libcppotp/otp.h

@ -0,0 +1,37 @@
/**
* @file otp.h
*
* @brief One-time-password-related functions.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_OTP_H__
#define __CPPTOTP_OTP_H__
#include "bytes.h"
#include "sha1.h"
#include <cstdint>
namespace CppTotp
{
/** The 64-bit-blocksize variant of HMAC-SHA1. */
Bytes::ByteString hmacSha1_64(const Bytes::ByteString & key, const Bytes::ByteString & msg);
/**
* Calculate the HOTP value of the given key, message and digit count.
*/
//uint32_t hotp(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
uint32_t hotp(const Bytes::ByteString & key, uint64_t counter, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
/**
* Calculate the TOTP value from the given parameters.
*/
uint32_t totp(const Bytes::ByteString & key, uint64_t timeNow, uint64_t timeStart, uint64_t timeStep, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
}
#endif

224
libcppotp/sha1.cpp

@ -0,0 +1,224 @@
/**
* @file sha1.cpp
*
* @brief Implementation of the SHA-1 hash.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "bytes.h"
#include <iostream>
#include <cassert>
namespace CppTotp
{
static inline uint32_t lrot32(uint32_t num, uint8_t rotcount)
{
return (num << rotcount) | (num >> (32 - rotcount));
}
Bytes::ByteString sha1(const Bytes::ByteString & msg)
{
const size_t size_bytes = msg.size();
const uint64_t size_bits = size_bytes * 8;
Bytes::ByteString bstr = msg;
Bytes::ByteStringDestructor asplode(&bstr);
// the size of msg in bits is always even. adding the '1' bit will make
// it odd and therefore incongruent to 448 modulo 512, so we can get
// away with tacking on 0x80 and then the 0x00s.
bstr.push_back(0x80);
while (bstr.size() % (512/8) != (448/8))
{
bstr.push_back(0x00);
}
// append the size in bits (uint64be)
bstr.append(Bytes::u64beToByteString(size_bits));
assert(bstr.size() % (512/8) == 0);
// initialize the hash counters
uint32_t h0 = 0x67452301;
uint32_t h1 = 0xEFCDAB89;
uint32_t h2 = 0x98BADCFE;
uint32_t h3 = 0x10325476;
uint32_t h4 = 0xC3D2E1F0;
// for each 64-byte chunk
for (size_t i = 0; i < bstr.size()/64; ++i)
{
Bytes::ByteString chunk(bstr.begin() + i*64, bstr.begin() + (i+1)*64);
Bytes::ByteStringDestructor xplode(&chunk);
uint32_t words[80];
size_t j;
// 0-15: the chunk as a sequence of 32-bit big-endian integers
for (j = 0; j < 16; ++j)
{
words[j] =
(chunk[4*j + 0] << 24) |
(chunk[4*j + 1] << 16) |
(chunk[4*j + 2] << 8) |
(chunk[4*j + 3] << 0)
;
}
// 16-79: derivatives of 0-15
for (j = 16; j < 32; ++j)
{
// unoptimized
words[j] = lrot32(words[j-3] ^ words[j-8] ^ words[j-14] ^ words[j-16], 1);
}
for (j = 32; j < 80; ++j)
{
// Max Locktyuchin's optimization (SIMD)
words[j] = lrot32(words[j-6] ^ words[j-16] ^ words[j-28] ^ words[j-32], 2);
}
// initialize hash values for the round
uint32_t a = h0;
uint32_t b = h1;
uint32_t c = h2;
uint32_t d = h3;
uint32_t e = h4;
// the loop
for (j = 0; j < 80; ++j)
{
uint32_t f = 0, k = 0;
if (j < 20)
{
f = (b & c) | ((~ b) & d);
k = 0x5A827999;
}
else if (j < 40)
{
f = b ^ c ^ d;
k = 0x6ED9EBA1;
}
else if (j < 60)
{
f = (b & c) | (b & d) | (c & d);
k = 0x8F1BBCDC;
}
else if (j < 80)
{
f = b ^ c ^ d;
k = 0xCA62C1D6;
}
else
{
assert(0 && "how did I get here?");
}
uint32_t tmp = lrot32(a, 5) + f + e + k + words[j];
e = d;
d = c;
c = lrot32(b, 30);
b = a;
a = tmp;
}
// add that to the result so far
h0 += a;
h1 += b;
h2 += c;
h3 += d;
h4 += e;
}
// assemble the digest
Bytes::ByteString first = Bytes::u32beToByteString(h0);
Bytes::ByteStringDestructor x1(&first);
Bytes::ByteString second = Bytes::u32beToByteString(h1);
Bytes::ByteStringDestructor x2(&second);
Bytes::ByteString third = Bytes::u32beToByteString(h2);
Bytes::ByteStringDestructor x3(&third);
Bytes::ByteString fourth = Bytes::u32beToByteString(h3);
Bytes::ByteStringDestructor x4(&fourth);
Bytes::ByteString fifth = Bytes::u32beToByteString(h4);
Bytes::ByteStringDestructor x5(&fifth);
return first + second + third + fourth + fifth;
}
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize = 64);
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize)
{
Bytes::ByteString realKey = key;
Bytes::ByteStringDestructor asplode(&realKey);
if (realKey.size() > blockSize)
{
// resize by calculating hash
Bytes::ByteString newRealKey = sha1(realKey);
Bytes::swizzleByteStrings(&realKey, &newRealKey);
}
if (realKey.size() < blockSize)
{
// pad with zeroes
realKey.resize(blockSize, 0x00);
}
// prepare the pad keys
Bytes::ByteString innerPadKey = realKey;
Bytes::ByteStringDestructor xplodeI(&innerPadKey);
Bytes::ByteString outerPadKey = realKey;
Bytes::ByteStringDestructor xplodeO(&outerPadKey);
// transform the pad keys
for (size_t i = 0; i < realKey.size(); ++i)
{
innerPadKey[i] = innerPadKey[i] ^ 0x36;
outerPadKey[i] = outerPadKey[i] ^ 0x5c;
}
// sha1(outerPadKey + sha1(innerPadKey + msg))
Bytes::ByteString innerMsg = innerPadKey + msg;
Bytes::ByteStringDestructor xplodeIM(&innerMsg);
Bytes::ByteString innerHash = sha1(innerMsg);
Bytes::ByteStringDestructor xplodeIH(&innerHash);
Bytes::ByteString outerMsg = outerPadKey + innerHash;
Bytes::ByteStringDestructor xplodeOM(&outerMsg);
return sha1(outerMsg);
}
}
#if TEST_SHA1
int main(void)
{
using namespace CppTotp;
const uint8_t * strEmpty = reinterpret_cast<const uint8_t *>("");
const uint8_t * strDog = reinterpret_cast<const uint8_t *>("The quick brown fox jumps over the lazy dog");
const uint8_t * strCog = reinterpret_cast<const uint8_t *>("The quick brown fox jumps over the lazy cog");
const uint8_t * strKey = reinterpret_cast<const uint8_t *>("key");
Bytes::ByteString shaEmpty = sha1(Bytes::ByteString(strEmpty));
Bytes::ByteString shaDog = sha1(Bytes::ByteString(strDog));
Bytes::ByteString shaCog = sha1(Bytes::ByteString(strCog));
Bytes::ByteString hmacShaEmpty = hmacSha1(Bytes::ByteString(), Bytes::ByteString());
Bytes::ByteString hmacShaKeyDog = hmacSha1(strKey, strDog);
std::cout
<< (Bytes::toHexString(shaEmpty) == "da39a3ee5e6b4b0d3255bfef95601890afd80709") << std::endl
<< (Bytes::toHexString(shaDog) == "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") << std::endl
<< (Bytes::toHexString(shaCog) == "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3") << std::endl
<< std::endl
<< (Bytes::toHexString(hmacShaEmpty) == "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d") << std::endl
<< (Bytes::toHexString(hmacShaKeyDog) == "de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9") << std::endl
<< std::endl;
return 0;
}
#endif

34
libcppotp/sha1.h

@ -0,0 +1,34 @@
/**
* @file sha1.h
*
* @brief The SHA-1 hash function.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_SHA1_H__
#define __CPPTOTP_SHA1_H__
#include "bytes.h"
namespace CppTotp
{
typedef Bytes::ByteString (*HmacFunc)(const Bytes::ByteString &, const Bytes::ByteString &);
/**
* Calculate the SHA-1 hash of the given message.
*/
Bytes::ByteString sha1(const Bytes::ByteString & msg);
/**
* Calculate the HMAC-SHA-1 hash of the given key/message pair.
*
* @note Most services assume a block size of 64.
*/
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize = 64);
}
#endif

19
main.cpp

@ -0,0 +1,19 @@
#include <QApplication>
#include <QSettings>
#include "mainwindow.h"
int main(int argc, char *argv[]) {
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
app.setOrganizationName("twofap");
app.setApplicationName("twofap");
app.setApplicationVersion("1.0");
QSettings::setDefaultFormat(QSettings::IniFormat);
MainWindow w;
w.setWindowTitle(app.applicationName() + " " + app.applicationVersion());
w.show();
return app.exec();
}

187
mainwindow.cpp

@ -0,0 +1,187 @@
#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)");
}

25
mainwindow.h

@ -0,0 +1,25 @@
#include <QMainWindow>
#include <QSettings>
class QTimer;
class QLabel;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
signals:
void addKeyClicked();
private slots:
void onAddKeyClicked();
void rebuildLayout();
private:
QWidget *m_centralWidget;
QSettings m_settings;
QLabel *m_statusLabel;
QTimer *m_timer;
void saveNewKey(QString name, QString key);
QString generateCode(QString key);
};

6
twofap.pro

@ -0,0 +1,6 @@
QT = core gui widgets
CONFIG += c++11
SOURCES = main.cpp mainwindow.cpp libcppotp/bytes.cpp libcppotp/otp.cpp libcppotp/sha1.cpp
HEADERS = mainwindow.h
Loading…
Cancel
Save