From fdda56c95ae249c90b47c1a060d081db66a730da Mon Sep 17 00:00:00 2001 From: Walker Thompson Date: Thu, 18 Dec 2025 23:13:27 +0000 Subject: Initial commit --- Makefile | 32 +++++++++ Makefile.MGW32 | 40 +++++++++++ crypto.cpp | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++ totpgen.h | 76 ++++++++++++++++++++ wxgui.cpp | 216 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 564 insertions(+) create mode 100644 Makefile create mode 100644 Makefile.MGW32 create mode 100644 crypto.cpp create mode 100644 totpgen.h create mode 100644 wxgui.cpp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a08b4e --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +UNAME = $(shell uname) + +CXX = g++ +PREFIX = /usr +WXCXXFLAGS = `wx-config --cxxflags` +WXLDFLAGS = `wx-config --libs` + +ifeq ($(UNAME),FreeBSD) + CXX := clang++ + PREFIX := /usr/local + WXCXXFLAGS := `wxgtk3u-3.2-config --cxxflags` + WXLDFLAGS := `wxgtk3u-3.2-config --libs` +endif + +CXXFLAGS = -Wall -Wextra -Werror -std=c++11 -I. -I$(PREFIX)/include $(WXCXXFLAGS) +LDFLAGS = -L$(PREFIX)/lib -lcrypto $(WXLDFLAGS) + +OUT = totpgen +OBJS = wxgui.o crypto.o +all: $(OBJS) + $(CXX) $(OBJS) -o $(OUT) $(LDFLAGS) + +crypto.o: crypto.cpp + $(CXX) $(CXXFLAGS) -c crypto.cpp -o crypto.o + +wxgui.o: wxgui.cpp + $(CXX) $(CXXFLAGS) -c wxgui.cpp -o wxgui.o + +.PHONY: clean +clean: + rm -f *.o *.a */*.o */*.a $(OUT) + diff --git a/Makefile.MGW32 b/Makefile.MGW32 new file mode 100644 index 0000000..496b117 --- /dev/null +++ b/Makefile.MGW32 @@ -0,0 +1,40 @@ +CXX = x86_64-w64-mingw32-c++ +AR = x86_64-w64-mingw32-ar +CXX_LDD = x86_64-w64-mingw32-ld +LDD = x86_64-w64-mingw32-ld + +WXINCLUDES = -Iwxmsw/include/x86_64-w64-mingw32-msw-unicode-3.2 -Iwxmsw/include +WXCXXFLAGS = -D_FILE_OFFSET_BITS=64 -DWXUSINGDLL -D__WXMSW__ -pthread $(WXINCLUDES) + +CXXFLAGS = -static -std=c++11 -lwinpthread -I. -I/usr/lib/gcc/x86_64-w64-mingw32/12-win32/include $(WXCXXFLAGS) + +WIN32_DLL_DIR = win32_dlls + +MGWLIBS = $(wildcard $(WIN32_DLL_DIR)/libgcc*.dll) $(wildcard $(WIN32_DLL_DIR)/libstd*.dll) + +WXLIBS = $(wildcard $(WIN32_DLL_DIR)/wx*.dll) +WXLDFLAGS = $(addprefix -l, $(basename $(WXLIBS))) + +SSLLIBS = $(wildcard $(WIN32_DLL_DIR)/libcrypto*.dll) +SSLLDFLAGS = $(addprefix -l, $(basename $(SSLLIBS))) + +LDFLAGS = -L. -Wl,-Bdynamic $(WXLDFLAGS) $(SSLLDFLAGS) + +OUT = totpgen +OBJS = wxgui.o crypto.o + +all: $(OUT) + +$(OUT): $(OBJS) + $(CXX) $(OBJS) -o $(OUT) $(LDFLAGS) + mkdir -p release && cp -r $(OUT).exe $(MGWLIBS) $(WXLIBS) $(SSLLIBS) release + +crypto.o: crypto.cpp + $(CXX) $(CXXFLAGS) -c crypto.cpp -o crypto.o + +wxgui.o: wxgui.cpp + $(CXX) $(CXXFLAGS) -c wxgui.cpp -o wxgui.o + +.PHONY: clean +clean: + rm -f *.o *.a */*.o */*.a $(OUT).exe release/* diff --git a/crypto.cpp b/crypto.cpp new file mode 100644 index 0000000..818f68a --- /dev/null +++ b/crypto.cpp @@ -0,0 +1,200 @@ +extern "C" { +#include +#include +#include +#include +} + +#include "totpgen.h" + +#define UNUSED(x) (void(x)) + +#define SALT_BYTES 8 +#define KEY_BYTES 32 +#define IV_BYTES 16 + + +#define AES_BUFFER(LEN) \ + reinterpret_cast( \ + calloc(AES_BLOCK_SIZE * (1 + ((LEN) / AES_BLOCK_SIZE)), \ + sizeof(unsigned char))); + +bool +TOTPGen::GenTOTP(std::string& secret_str) +{ + uint32_t bits, code, count, hmacsize; + uint64_t clock; + size_t length; + uint8_t msg[8], hmac[EVP_MAX_MD_SIZE]; + char totp_buf[7]; + + size_t keysize = 0; + const EVP_MD *digest = EVP_sha1(); + uint8_t *key = NULL; + + char *secret = strdup(secret_str.c_str()); + + for (count = bits = length = 0; *secret; secret++) { + if (!strchr(base32.c_str(), *secret)) { + return false; + } + bits = (bits << 5) | (strchr(base32.c_str(), *secret) - base32.c_str()); + count += 5; + + while (count >= 8) { + while (length >= keysize) { + keysize = keysize ? keysize << 1 : 64; + if (!(key = (uint8_t *)realloc(key, keysize))) { + return false; + } + } + count -= 8, key[length++] = bits >> count; + } + } + + if (length > 0) { + clock = time(NULL) / 30; + + for (count = 0; count < 8; count++) { + msg[7 - count] = clock >> 8 * count; + } + if (!HMAC(digest, key, length, msg, sizeof(msg), hmac, &hmacsize)) { + return false; + } + + for (code = count = 0; count < 4; count++) { + code += hmac[(hmac[hmacsize - 1] & 0x0f) + 3 - count] << 8 * count; + } + code &= 0x7fffffff; + + snprintf((char *)&totp_buf, 7, "%06u", code % 1000000); + } + else { + return false; + } + + TOTP = totp_buf; + TOTPField->SetLabel(TOTP); + + free(key); + + return true; +} + +void +TOTPGen::InitAES(const char *passphrase, unsigned char *salt, + unsigned char *key, unsigned char *iv) +{ + char *key_data = strdup(passphrase); + int key_text_len = strlen(passphrase); + int nrounds = 0xFF; + + EVP_BytesToKey(EVP_aes_256_cbc(), EVP_sha1(), salt, + reinterpret_cast(key_data), + key_text_len, nrounds, key, iv); + + free(key_data); +} + +bool +TOTPGen::EncryptSecret(std::string& secret_str) +{ + unsigned char salt[SALT_BYTES], key[KEY_BYTES], iv[IV_BYTES]; + + RAND_bytes(salt, SALT_BYTES); + + memset(&key, 0, KEY_BYTES); + memset(&iv, 0, IV_BYTES); + + InitAES(Passphrase.c_str(), reinterpret_cast(&salt), + reinterpret_cast(&key), + reinterpret_cast(&iv)); + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, + reinterpret_cast(&key), + reinterpret_cast(&iv)); + + size_t plaintext_len = strlen(secret_str.c_str()) ; + + unsigned char *plaintext = AES_BUFFER(plaintext_len); + strncpy(reinterpret_cast(plaintext), + secret_str.c_str(), plaintext_len); + + unsigned char *ciphertext = AES_BUFFER(plaintext_len); + + int len, ciphertext_len; + EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len); + ciphertext_len = len; + EVP_EncryptFinal_ex(ctx, ciphertext + len, &len); + ciphertext_len += len; + + FILE *secret_file = fopen(SecretFile.c_str(), "wb"); + if (!secret_file) { + return false; + } + + fwrite(salt, SALT_BYTES, 1, secret_file); + fwrite(ciphertext, ciphertext_len, 1, secret_file); + + fclose(secret_file); + + EVP_CIPHER_CTX_free(ctx); + free(plaintext); + free(ciphertext); + + return true; +} + + +bool +TOTPGen::DecryptSecret(void) +{ + unsigned char salt[SALT_BYTES], key[KEY_BYTES], iv[IV_BYTES]; + + memset(&key, 0, KEY_BYTES); + memset(&iv, 0, IV_BYTES); + + FILE *secret_file = fopen(SecretFile.c_str(), "rb"); + if (!secret_file) { + return false; + } + + fseek(secret_file, 0L, SEEK_END); + size_t ciphertext_len = ftell(secret_file) - SALT_BYTES; + rewind(secret_file); + + fread(&salt, SALT_BYTES, 1, secret_file); + + InitAES(Passphrase.c_str(), reinterpret_cast(&salt), + reinterpret_cast(&key), + reinterpret_cast(&iv)); + + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, + reinterpret_cast(&key), + reinterpret_cast(&iv)); + + int len; + unsigned char *ciphertext = AES_BUFFER(ciphertext_len); + unsigned char *plaintext = AES_BUFFER(ciphertext_len + 8); + + fread(ciphertext, 1, ciphertext_len, secret_file); + fclose(secret_file); + + if (!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) { + return false; + } + + if (!EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) { + return false; + } + + Secret = reinterpret_cast(plaintext); + + EVP_CIPHER_CTX_free(ctx); + free(ciphertext); + free(plaintext); + + return true; +} diff --git a/totpgen.h b/totpgen.h new file mode 100644 index 0000000..f1a368d --- /dev/null +++ b/totpgen.h @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +static const std::string base32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +class TOTPGenGUI : public wxApp { + public: + virtual bool OnInit(); +}; + +class TOTPGen : public wxFrame { + public: + TOTPGen(); + + private: + wxPanel *MainWindow; + wxBoxSizer *MainSizer; + + wxBoxSizer *ButtonSizer_Upper; + wxBoxSizer *ButtonSizer_Lower; + + wxButton *UpdateSecretButton; + void UpdateSecret(wxCommandEvent& event); + + bool Locked; + wxButton *UnlockButton; + void Unlock(wxCommandEvent& event); + + wxButton *CopyButton; + void CopyText(wxCommandEvent& event); + + wxButton *QuitButton; + void OnExit(wxCommandEvent& event); + + wxBoxSizer *TextSizer; + wxStaticText *TOTPField; + + wxString SecretFile; + + void InitAES(const char *passphrase, + unsigned char *salt, + unsigned char *key, + unsigned char *iv); + + wxTimer *CountdownTimer; + wxGauge *CountdownGauge; + void UpdateStatus(wxTimerEvent& event); + + std::string Secret; + std::string Passphrase; + + bool IsBase32(std::string& str); + + bool DecryptSecret(void); + bool EncryptSecret(std::string& secret_str); + + std::string TOTP; + + bool GenTOTP(std::string& secret_str); +}; \ No newline at end of file diff --git a/wxgui.cpp b/wxgui.cpp new file mode 100644 index 0000000..6a6873a --- /dev/null +++ b/wxgui.cpp @@ -0,0 +1,216 @@ +#include "totpgen.h" + +#ifdef _WIN32 +static const std::string path_separator = "\\"; +static const std::string config_dir_name = "TOTPGen"; +#else +static const std::string path_separator = "/"; +static const std::string config_dir_name = ".totpgen"; +#endif + +IMPLEMENT_APP(TOTPGenGUI) + +bool TOTPGenGUI::OnInit() +{ + TOTPGen *MainAppFrame = new TOTPGen(); + MainAppFrame->Show(true); + return true; +} + +TOTPGen::TOTPGen() : wxFrame(nullptr, wxID_ANY, wxT("TOTP Generator"), + wxDefaultPosition, wxSize(320,240), + wxDEFAULT_FRAME_STYLE | wxRESIZE_BORDER) +{ + MainWindow = new wxPanel(this, wxID_ANY); + MainSizer = new wxBoxSizer(wxVERTICAL); + + Locked = true; + + wxString UserConfigDir = wxStandardPaths::Get().GetUserConfigDir(); + + wxString AppConfigDir = UserConfigDir + path_separator + config_dir_name; + if (!wxDirExists(AppConfigDir)) { + wxMkdir(AppConfigDir); + } + + SecretFile = AppConfigDir + path_separator + "secret.dat"; + + TOTPField = new wxStaticText(MainWindow, wxID_ANY, + wxEmptyString, wxDefaultPosition, + wxDefaultSize, wxTE_CENTER); + wxFont LabelFont(48, wxFONTFAMILY_TELETYPE, + wxFONTSTYLE_NORMAL, + wxFONTWEIGHT_NORMAL); + TOTPField->SetFont(LabelFont); + TOTPField->SetLabel(" "); + + TextSizer = new wxBoxSizer(wxHORIZONTAL); + TextSizer->Add(TOTPField, 2, wxCENTER); + + CountdownGauge = new wxGauge(MainWindow, wxID_ANY, 30, wxPoint(50, 50)); + CountdownTimer = new wxTimer(MainWindow, wxID_ANY); + CountdownTimer->Start(1000); + + UpdateSecretButton = new wxButton(MainWindow, wxID_OK, wxT("Set Key")); + UpdateSecretButton->Bind(wxEVT_BUTTON, &TOTPGen::UpdateSecret, this); + + UnlockButton = new wxButton(MainWindow, wxID_CLEAR, wxT("Unlock")); + UnlockButton->Bind(wxEVT_BUTTON, &TOTPGen::Unlock, this); + if (!wxFileExists(SecretFile)) { + UnlockButton->Enable(false); + } + + ButtonSizer_Upper = new wxBoxSizer(wxHORIZONTAL); + ButtonSizer_Upper->Add(UpdateSecretButton, 2, wxEXPAND | wxALL); + ButtonSizer_Upper->Add(UnlockButton, 2, wxEXPAND | wxALL); + + CopyButton = new wxButton(MainWindow, wxID_COPY, wxT("Copy")); + CopyButton->Bind(wxEVT_BUTTON, &TOTPGen::CopyText, this); + if (Locked) { + CopyButton->Enable(false); + } + + QuitButton = new wxButton(MainWindow, wxID_EXIT, wxT("Exit")); + QuitButton->Bind(wxEVT_BUTTON, &TOTPGen::OnExit, this); + + ButtonSizer_Lower = new wxBoxSizer(wxHORIZONTAL); + ButtonSizer_Lower->Add(CopyButton, 2, wxEXPAND | wxALL); + ButtonSizer_Lower->Add(QuitButton, 2, wxEXPAND | wxALL); + + MainSizer->Add(TextSizer, 2, wxCENTER | wxALIGN_CENTER); + MainSizer->Add(CountdownGauge, 2, wxEXPAND | wxALL); + MainSizer->Add(ButtonSizer_Upper, 2, wxEXPAND | wxALL); + MainSizer->Add(ButtonSizer_Lower, 2, wxEXPAND | wxALL); + + MainWindow->SetSizer(MainSizer); +} + +void TOTPGen::OnExit(wxCommandEvent& WXUNUSED(event)) +{ + Close(true); +} + +bool TOTPGen::IsBase32(std::string& str) +{ + std::unordered_set Base32Chars(base32.begin(), base32.end()); + + for (char c: str) { + if (Base32Chars.find(c) == Base32Chars.end()) { + return false; + } + } + + return true; +} + +void TOTPGen::UpdateSecret(wxCommandEvent& WXUNUSED(event)) +{ + wxTextEntryDialog SecretPrompt(this, wxT("Please enter the base 32 token."), + wxT("Secret Entry"), wxEmptyString); + + std::string Secret_New; + if (SecretPrompt.ShowModal() == wxID_OK) { + Secret_New = SecretPrompt.GetValue().MakeUpper().ToStdString(); + if (!IsBase32(Secret_New)) { + wxMessageBox(wxT("Base 32 token contains invalid characters.\n" + "Only the following are permissible: \n" + base32), + wxT("Error"), wxOK | wxICON_ERROR); + return; + } + } + + wxPasswordEntryDialog PasswordPrompt(this, wxT("Please enter a new passphrase\n" + "to encrypt the secret token."), + wxT("Passphrase Entry"), wxEmptyString); + + wxPasswordEntryDialog ConfirmPrompt(this, wxT("Please confirm the passphrase."), + wxT("Passphrase Entry"), wxEmptyString); + + + if ((PasswordPrompt.ShowModal() == wxID_OK) + && (ConfirmPrompt.ShowModal() == wxID_OK)) { + if (PasswordPrompt.GetValue() == ConfirmPrompt.GetValue()) { + Passphrase = PasswordPrompt.GetValue(); + } + else { + wxMessageBox(wxT("Passphrases did not match - try again"), + wxT("Error"), wxOK | wxICON_ERROR); + return; + } + } + else { + return; + } + + + if (!GenTOTP(Secret_New)) { + wxMessageBox(wxT("Could not generate TOTP - bad token?"), + wxT("Error"), wxOK | wxICON_ERROR); + return; + } + + Secret = Secret_New; + EncryptSecret(Secret); + + if (Locked) { + Locked = false; + + MainWindow->Connect(CountdownTimer->GetId(), wxEVT_TIMER, + wxTimerEventHandler(TOTPGen::UpdateStatus), + NULL, this); + + CopyButton->Enable(true); + UnlockButton->Enable(false); + } + + return; +} + +void TOTPGen::Unlock(wxCommandEvent& WXUNUSED(event)) +{ + wxPasswordEntryDialog PasswordPrompt(this, wxT("Please enter your passphrase."), + wxT("Passphrase Entry"), wxEmptyString); + + if (PasswordPrompt.ShowModal() == wxID_OK) { + Passphrase = PasswordPrompt.GetValue(); + } + + if (!DecryptSecret()) { + wxMessageBox(wxT("Could not unlock base 32 token,\n" + "bad passphrase or corrupt file?"), + wxT("Error"), wxOK | wxICON_ERROR); + return; + } + + MainWindow->Connect(CountdownTimer->GetId(), wxEVT_TIMER, + wxTimerEventHandler(TOTPGen::UpdateStatus), + NULL, this); + + Locked = false; + + CopyButton->Enable(true); + UnlockButton->Enable(false); + + GenTOTP(Secret); +} + +void TOTPGen::UpdateStatus(wxTimerEvent& WXUNUSED(event)) +{ + unsigned char CounterValue = 29 - ((time(NULL) - 1) % 30); + + CountdownGauge->SetValue(CounterValue); + + if (CounterValue == 0) { + GenTOTP(Secret); + } +} + +void TOTPGen::CopyText(wxCommandEvent& WXUNUSED(event)) +{ + wxString ToCopy = TOTPField->GetLabel(); + + if (wxTheClipboard->Open() && !Locked) { + wxTheClipboard->SetData(new wxTextDataObject(ToCopy)); + wxTheClipboard->Close(); + } +} \ No newline at end of file -- cgit v1.3