commit 81a90a99e1cf4c79ec1df1258b9ca4ad3fc7b5e1 Author: 諏訪子 Date: Wed Jan 21 04:47:40 2026 +0900 SVNからのミラー diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e80a20e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# 1.1.1 (2025年03月13日) +* OTPで、秘密鍵を自動的に大文字にする様に + +# 1.1.0 (2024年12月17日) +* パスワード表示で、「OpenPGP」かどうかの確認の追加 +* 侵害されたパスワードの確認の追加 +* 複数サイトで同じパスワードを利用かどうか、パスワードの長さ、又はパスワードの強さの確認 +* パスワードを表示・非表示にする機能性の追加(デフォルトは非表示) +* ワンタイムパスワード(OTP)の場合、自動的に更新する様に +* アプリケーションアイコンの追加 +* ライトモードの追加 +* コンフィグファイルの追加 + +# 1.0.0 (2024年09月24日) +* 開始 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..777a97f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright © 2018-2024 by 076.moe + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a34f228 --- /dev/null +++ b/Makefile @@ -0,0 +1,126 @@ +UNAME_S != uname -s +UNAME_M != uname -m +OS = ${UNAME_S} +ARCH = ${UNAME_M} + +.if ${UNAME_S} == "OpenBSD" +OS = openbsd +.elif ${UNAME_S} == "NetBSD" +OS = netbsd +.elif ${UNAME_S} == "FreeBSD" +OS = freebsd +.elif ${UNAME_S} == "Dragonfly" +OS = dragonfly +.elif ${UNAME_S} == "Linux" +OS = linux +.endif + +.if ${UNAME_M} == "x86_64" +ARCH = amd64 +.endif + +NAME != cat main.cc | grep "const char \*sofname" | awk '{print $$5}' |\ + sed "s/\"//g" | sed "s/;//" +VERSION != cat main.cc | grep "const char \*version" | awk '{print $$5}' |\ + sed "s/\"//g" | sed "s/;//" + +PREFIX = /usr/local +.if ${OS} == "linux" +PREFIX = /usr +.endif + +CC = c++ +FILES = main.cc src/*.cc + +.if ${OS} == "openbsd" +DEPS = pkg_add fltk gpgme gpgme-qt gnupg pinentry +.elif ${OS} == "netbsd" +DEPS = pkgin install fltk gpgme gpgmepp gnupg pinentry pinentry-fltk +.elif ${OS} == "freebsd" +DEPS = pkg install fltk gpgme gpgme-cpp gnupg pinentry pinentry-fltk +.elif ${OS} == "linux" +DEPS = xbps-install fltk fltk-devel gpgme gpgmepp gpgmepp-devel gnupg pinentry bmake +.endif + +CFLAGS = -Wall -Wextra -Wno-non-c-typedef-for-linkage -Wno-unused-parameter\ + -Wno-cast-function-type\ + -I/usr/include -L/usr/lib +.if ${OS} == "freebsd" || ${OS} == "openbsd" || ${OS} == "netbsd" || ${OS} == "dragonfly +CFLAGS += -I/usr/local/include -L/usr/local/lib +.endif +.if ${OS} == "netbsd" +CFLAGS += -I/usr/X11R7/include -L/usr/X11R7/lib -I/usr/pkg/include -L/usr/pkg/lib +.elif ${OS} == "openbsd" +CFLAGS += -I/usr/X11R6/include -L/usr/X11R6/lib +.endif + +LDFLAGS = -lfltk_images -lfltk -lX11 -lassuan -lgpgmepp -lgpgme -lcrypto -lgpg-error + +.if ${OS} == "openbsd" +LDFLAGS += -lc++abi -lpthread -lm -lc\ + -lXcursor -lXfixes -lXext -lXft -lfontconfig -lXinerama -lXdmcp -lXau\ + -lz -lxcb -lXrender -lexpat -lfreetype -lc++ -lintl -liconv -lpng -ljpeg +.elif ${OS} == "freebsd" +LDFLAGS += -lcxxrt -lm -lXrender -lXcursor -lXfixes -lXext -lXft -lfontconfig\ + -lXinerama -lthr -lz -lxcb -lfreetype -lexpat -lXau -lXdmcp\ + -lbz2 -lbrotlidec -lbrotlicommon -lc++ -lgcc -lc -lgpgme -lassuan\ + -lintl -lpng16 +.elif ${OS} == "netbsd" +LDFLAGS += -lstdc++ -lpthread -lm -lc -lXft -lxcb -lfontconfig -lfreetype\ + -lXau -lXdmcp -lXcursor -lXrandr -lXext -lXrender -lXfixes -lXinerama -lX11\ + -lexpat -lz -lbz2 -lgcc -lassuan -lintl +.elif ${OS} == "linux" +LDFLAGS += -lstdc++ -lgcc -lc -lXft -lXext -lXrender -lfontconfig -lXinerama\ + -lxcb -lfreetype -lpng16 -lz -lexpat -lXau -lXdmcp -lbz2\ + -lbrotlidec -lbrotlicommon -lassuan +.endif + +all: + ${CC} -O3 ${CFLAGS} -o ${NAME}\ + ${FILES} -static ${LDFLAGS} + strip ${NAME} + +depend: + ${DEPS} + +debug: + ${CC} -g ${CFLAGS} -o ${NAME} ${FILES} ${LDFLAGS} + +clean: + rm -rf ${NAME} + +dist: + mkdir -p ${NAME}-${VERSION} release/src release/desktop + cp -R LICENSE.txt Makefile README.md CHANGELOG.md logo.png\ + main.* src icons ${NAME}.desktop ${NAME}-${VERSION} + tar zcfv release/src/${NAME}-${VERSION}.tar.gz ${NAME}-${VERSION} + cp ${NAME}.desktop release/desktop + rm -rf ${NAME}-${VERSION} + +release: + mkdir -p release/bin/${VERSION}/${OS}/${ARCH} + ${CC} -O3 ${CFLAGS} -o release/bin/${VERSION}/${OS}/${ARCH}/${NAME} ${FILES}\ + -static ${LDFLAGS} + strip release/bin/${VERSION}/${OS}/${ARCH}/${NAME} + +publish: + rsync -rtvzP release/bin/${VERSION} 192.168.0.143:/zroot/repo/bin/${NAME} + rsync -rtvzP release/src/* 192.168.0.143:/zroot/repo/src/${NAME} + rsync -rtvzP release/desktop/* 192.168.0.143:/zroot/repo/desktop + rsync -rtvzP release/icons/* 192.168.0.143:/zroot/repo/icons + +install: + mkdir -p ${DESTDIR}${PREFIX}/bin ${DESTDIR}${PREFIX}/share/applications + cp -f ${NAME} ${DESTDIR}${PREFIX}/bin + cp -f ${NAME}.desktop ${DESTDIR}${PREFIX}/share/applications +.if ${OS} == "linux" + sed -i 's/\/local//' ${DESTDIR}${PREFIX}/share/applications/${NAME}.desktop +.endif + cp -rf icons/076 ${DESTDIR}${PREFIX}/share/icons + chmod 755 ${DESTDIR}${PREFIX}/bin/${NAME} + +uninstall: + rm -f ${DESTDIR}${PREFIX}/share/applications/${NAME}.desktop + rm -f ${DESTDIR}${PREFIX}/bin/${NAME} + +.PHONY: all debug clean dist release publish install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f4785d --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# simpas +シンプルなパスワードマネージャー Simple Password Manager\ +GUI版のsp + +## インストールする方法 | Installation +### BSD +```sh +doas make depends +make +doas make install +``` + +### Void Linux +```sh +sudo bmake depends +bmake +sudo bmake install +``` + +### Alpine Linux +```sh +sudo apk add fltk fltk-dev gpgme gpgmepp gnupg pinentry bmake +bmake +sudo bmake install +``` diff --git a/icons/076/128x128/simpas.png b/icons/076/128x128/simpas.png new file mode 100644 index 0000000..8060928 Binary files /dev/null and b/icons/076/128x128/simpas.png differ diff --git a/icons/076/16x16/simpas.png b/icons/076/16x16/simpas.png new file mode 100644 index 0000000..0154695 Binary files /dev/null and b/icons/076/16x16/simpas.png differ diff --git a/icons/076/18x18/simpas.png b/icons/076/18x18/simpas.png new file mode 100644 index 0000000..f511c28 Binary files /dev/null and b/icons/076/18x18/simpas.png differ diff --git a/icons/076/22x22/simpas.png b/icons/076/22x22/simpas.png new file mode 100644 index 0000000..0cedb40 Binary files /dev/null and b/icons/076/22x22/simpas.png differ diff --git a/icons/076/24x24/simpas.png b/icons/076/24x24/simpas.png new file mode 100644 index 0000000..0dcd38c Binary files /dev/null and b/icons/076/24x24/simpas.png differ diff --git a/icons/076/256x256/simpas.png b/icons/076/256x256/simpas.png new file mode 100644 index 0000000..d8ea652 Binary files /dev/null and b/icons/076/256x256/simpas.png differ diff --git a/icons/076/32x32/simpas.png b/icons/076/32x32/simpas.png new file mode 100644 index 0000000..963ff96 Binary files /dev/null and b/icons/076/32x32/simpas.png differ diff --git a/icons/076/42x42/simpas.png b/icons/076/42x42/simpas.png new file mode 100644 index 0000000..3538539 Binary files /dev/null and b/icons/076/42x42/simpas.png differ diff --git a/icons/076/48x48/simpas.png b/icons/076/48x48/simpas.png new file mode 100644 index 0000000..2ac684a Binary files /dev/null and b/icons/076/48x48/simpas.png differ diff --git a/icons/076/512x512/simpas.png b/icons/076/512x512/simpas.png new file mode 100644 index 0000000..ceccc27 Binary files /dev/null and b/icons/076/512x512/simpas.png differ diff --git a/icons/076/64x64/simpas.png b/icons/076/64x64/simpas.png new file mode 100644 index 0000000..31e1ac6 Binary files /dev/null and b/icons/076/64x64/simpas.png differ diff --git a/icons/076/84x84/simpas.png b/icons/076/84x84/simpas.png new file mode 100644 index 0000000..c6cfc63 Binary files /dev/null and b/icons/076/84x84/simpas.png differ diff --git a/icons/076/8x8/simpas.png b/icons/076/8x8/simpas.png new file mode 100644 index 0000000..2546de3 Binary files /dev/null and b/icons/076/8x8/simpas.png differ diff --git a/icons/076/96x96/simpas.png b/icons/076/96x96/simpas.png new file mode 100644 index 0000000..270a05b Binary files /dev/null and b/icons/076/96x96/simpas.png differ diff --git a/icons/076/index.theme b/icons/076/index.theme new file mode 100644 index 0000000..99dfbca --- /dev/null +++ b/icons/076/index.theme @@ -0,0 +1,75 @@ +[Icon Theme] +Name=076 Icons +Comment=076 Icons +Inherits=default +Directories=128x128,18x18,24x24,32x32,48x48,64x64,8x8,16x16,22x22,256x256,42x42,512x512,84x84,96x96 + +[128x128] +Size=128 +Context=Icons +Type=Fixed + +[18x18] +Size=18 +Context=Icons +Type=Fixed + +[24x24] +Size=24 +Context=Icons +Type=Fixed + +[32x32] +Size=32 +Context=Icons +Type=Fixed + +[48x48] +Size=48 +Context=Icons +Type=Fixed + +[64x64] +Size=64 +Context=Icons +Type=Fixed + +[8x8] +Size=8 +Context=Icons +Type=Fixed + +[16x16] +Size=16 +Context=Icons +Type=Fixed + +[22x22] +Size=22 +Context=Icons +Type=Fixed + +[256x256] +Size=256 +Context=Icons +Type=Fixed + +[42x42] +Size=42 +Context=Icons +Type=Fixed + +[512x512] +Size=512 +Context=Icons +Type=Fixed + +[84x84] +Size=84 +Context=Icons +Type=Fixed + +[96x96] +Size=96 +Context=Icons +Type=Fixed diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..c958da8 Binary files /dev/null and b/logo.png differ diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..b53993d --- /dev/null +++ b/main.cc @@ -0,0 +1,362 @@ +#include "src/addpass.hh" +#include "src/delpass.hh" +#include "src/editpass.hh" +#include "src/genpass.hh" +#include "src/initpass.hh" +#include "src/showpass.hh" +#include "src/vulnpass.hh" +#include "src/chkpass.hh" +#include "src/common.hh" +#include "main.hh" + +#undef Status +#undef None +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +Fl_Select_Browser *browser = nullptr; +Fl_Text_Display *textview = nullptr; +Fl_Text_Buffer *textbuf = nullptr; +Fl_Input *searchfield = nullptr; +Fl_Check_Button *hidechk = nullptr; +Fl_Button *showbtn = nullptr; + +Addpass a; +Chkpass c; +Delpass d; +Editpass e; +Genpass g; +Initpass i; +Showpass s; +Vulnpass v; + +const char *sofname = "simpas"; +const char *intname = "SimPas"; +const char *version = "1.1.1"; +const char *basedof = "sp-1.5.1"; + +std::vector fullpaths; +std::vector dispaths; +std::vector filterpaths; +int browseId; +bool isPassHidden = true; +std::string realpass = ""; + +void browse(std::string &path, bool isNew) { + std::string cont = s.exec(path.c_str(), false); + realpass = cont; + + if (isNew) browseId = browser->size(); + browser->value(browseId); + + if (!cont.empty()) { + if (isPassHidden) { + std::string lang = Common::getlang(); + if (lang.compare(0, 2, "en") == 0) { + textbuf->text("(Hidden, please click the \"show\" button to reveal)"); + } else { + textbuf->text("(非表示、「表示」ボタンをクリックして確認して下さい)"); + } + showbtn->activate(); + showbtn->label((Common::getlang().compare(0, 2, "en") == 0) ? + "Show password" : "パスワードの表示"); + } else { + textbuf->text(cont.c_str()); + showbtn->label((Common::getlang().compare(0, 2, "en") == 0) ? + "Hide password" : "パスワードを隠す"); + showbtn->activate();} + Editpass::setFile(path); + Delpass::setFile(path); + d.btn->activate(); + e.btn->activate(); + } +} + +void hide_cb(Fl_Widget *w, void *) { + isPassHidden = ((Fl_Check_Button *)w)->value(); + int idx = browser->value(); + + if (isPassHidden && idx > 0) { + showbtn->activate(); + showbtn->label((Common::getlang().compare(0, 2, "en") == 0) ? + "Show password" : "パスワードの表示"); + } else { + showbtn->deactivate(); + showbtn->label((Common::getlang().compare(0, 2, "en") == 0) ? + "Hide password" : "パスワードを隠す"); + } + + showbtn->label((Common::getlang().compare(0, 2, "en") == 0) ? + "Show password" : "パスワードの表示"); + + if (idx > 0) { + std::string path = filterpaths[idx - 1]; + browse(path, false); + } +} + +void clearpaths(bool isReset, std::string &path) { + fullpaths.clear(); + dispaths.clear(); + if (isReset) { + std::string mockpath = ""; + Editpass::setFile(mockpath); + Delpass::setFile(mockpath); + d.btn->deactivate(); + e.btn->deactivate(); + } +} + +void updatelist() { + browser->clear(); + filterpaths.clear(); + std::string searchtxt = searchfield->value(); + for (size_t i = 0; i < dispaths.size(); ++i) { + if (dispaths[i].find(searchtxt) != std::string::npos) { + browser->add(dispaths[i].c_str()); + filterpaths.push_back(fullpaths[i]); + } + } + textbuf->text(""); +} + +void search_cb(Fl_Widget *, void *) { + std::string mockpath = ""; + Editpass::setFile(mockpath); + Delpass::setFile(mockpath); + d.btn->deactivate(); + e.btn->deactivate(); + updatelist(); +} + +void copy_cb(Fl_Widget *, void *) { + if (!realpass.empty()) { + Fl::copy(realpass.c_str(), realpass.length(), 1, Fl::clipboard_plain_text); + } else { + Fl::copy("", 0, 1, Fl::clipboard_plain_text); + } +} + +void browser_cb(Fl_Widget *w, void *) { + (void)w; + int idx = browser->value(); + if (idx == 0) return; + browseId = idx; + + std::string path = filterpaths[idx - 1]; + browse(path, false); +} + +void show_cb(Fl_Widget *, void *) { + int idx = browser->value(); + if (idx == 0) return; + std::string path = filterpaths[idx - 1]; + + if (isPassHidden) { + std::string cont = s.exec(path.c_str(), false); + realpass = cont; + textbuf->text(cont.c_str()); + + std::string lang = Common::getlang(); + if (lang.compare(0, 2, "en") == 0) { + showbtn->label("Hide password"); + } else { + showbtn->label("パスワードを隠す"); + } + isPassHidden = false; + } else { + std::string lang = Common::getlang(); + if (lang.compare(0, 2, "en") == 0) { + textbuf->text("(Hidden, please click the \"show\" button to reveal)"); + showbtn->label("Show password"); + } else { + textbuf->text("(非表示、「表示」ボタンをクリックして確認して下さい)"); + showbtn->label("パスワードの表示"); + } + isPassHidden = true; + } +} + +void scandir(const std::string &dpath, const std::string &rpath, + std::vector &fpaths) { + DIR *dir = opendir(dpath.c_str()); + if (!dir) return; + + struct dirent *entry; + while ((entry = readdir(dir)) != nullptr) { + std::string name = entry->d_name; + if (name == "." || name == ".." || name == ".gpg-id") continue; + + std::string fpath = std::string(dpath) + "/" + name; + struct stat s; + if (stat(fpath.c_str(), &s) != 0) { + closedir(dir); + return; + } + + if (S_ISDIR(s.st_mode)) { + scandir(fpath, rpath, fpaths); + } else if (name.find(".gpg") != std::string::npos) { + std::string rel = fpath.substr(rpath.size() + 1); + fpaths.push_back(rel); + fullpaths.push_back(fpath); + + std::string disname = rel.substr(0, rel.rfind(".gpg")); + dispaths.push_back(disname); + } + } + + closedir(dir); +} + +void init_cb(Fl_Widget *w, void *data) { + i.exec(i.gpgid->value()); + i.btn->deactivate(); + ((Initpass *)data)->cancel_cb(w, data); +} + +void init_dialog_cb(Fl_Widget *w, void *) { + (void)w; + std::string lang = Common::getlang(); + Fl_Window *dialog = new Fl_Window(450, 120, + (lang.compare(0, 2, "en") == 0 ? + "Initialize password" : "パスワードの初期設定")); + + i.gpgid = new Fl_Input(90, 20, 300, 30, + (lang.compare(0, 2, "en") == 0 ? "GPG secret key:" : "gpg秘密鍵:")); + dialog->add(i.gpgid); + + Fl_Button *startbtn = new Fl_Button(185, 70, 80, 30, + (lang.compare(0, 2, "en") == 0 ? "Start" : "開始")); + + startbtn->callback(init_cb, dialog); + + dialog->add(startbtn); + + dialog->end(); + dialog->set_modal(); + dialog->show(); +} + +void set_dark_theme() { + Fl::background(35, 32, 35); + Fl::background2(68, 59, 68); + Fl::foreground(252, 252, 252); +} + +int main(int argc, char **argv) { + std::string lang = Common::getlang(); + std::string windowtit = std::string(intname) + " " + version; + Fl_Window *window = new Fl_Window(790, 740, windowtit.c_str()); + + set_dark_theme(); + +#if defined(__linux) + const char *iconPath = "/usr/share/icons/076/512x512/simpas.png"; +#else + const char *iconPath = "/usr/local/share/icons/076/512x512/simpas.png"; +#endif + + Fl_PNG_Image *icon = new Fl_PNG_Image(iconPath); + + window->icon(icon); + + searchfield = new Fl_Input( + (lang.compare(0, 2, "en") == 0 ? 70 : 50), 10, + (lang.compare(0, 2, "en") == 0 ? 710 : 730), 30, + (lang.compare(0, 2, "en") == 0 ? "Search:" : "検索:")); + searchfield->callback(search_cb); + + browser = new Fl_Select_Browser(10, 50, 380, 500); + textview = new Fl_Text_Display(400, 50, 380, 500); + textbuf = new Fl_Text_Buffer(); + textview->buffer(textbuf); + + browser->callback(browser_cb); + + Fl_Button *copybtn = new Fl_Button(400, 600, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Copy password" : "パスワードのコピー")); + copybtn->callback(copy_cb); + + a.btn = new Fl_Button(10, 560, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Add password" : "パスワードの追加")); + a.btn->callback(a.dialog_cb); + + d.btn = new Fl_Button(10, 600, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Delete password" : "パスワードの削除")); + d.btn->deactivate(); + d.btn->callback(d.dialog_cb); + + e.btn = new Fl_Button(400, 560, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Edit password" : "パスワードの編集")); + e.btn->deactivate(); + e.btn->callback(e.dialog_cb); + + hidechk = new Fl_Check_Button(560, 560, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Hide password" : "パスワードを隠す")); + hidechk->set(); + hidechk->callback(hide_cb); + + showbtn = new Fl_Button(560, 600, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Show password" : "パスワードの表示")); + showbtn->deactivate(); + showbtn->callback(show_cb); + + g.btn = new Fl_Button(10, 640, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Generate password" : "パスワードの作成")); + g.btn->callback(g.dialog_cb); + + i.btn = new Fl_Button(10, 680, 150, 30, + (lang.compare(0, 2, "en") == 0 ? + "Initialize password" : + "パスワードの初期設定")); + + v.btn = new Fl_Button(170, 560, 200, 30, + (lang.compare(0, 2, "en") == 0 ? "Check for breach" : "漏洩されたかの確認")); + v.btn->callback(v.dialog_cb); + + c.btn = new Fl_Button(170, 600, 200, 30, + (lang.compare(0, 2, "en") == 0 ? + "Check for unsafe passwords" : "不安定的なパスワードの確認")); + c.btn->callback(c.dialog_cb); + + std::string gpgidpath = Common::getbasedir(true) + ".gpg-id"; + + struct stat buf; + if (stat(gpgidpath.c_str(), &buf) == 0) { + i.btn->deactivate(); + } + i.btn->callback(init_dialog_cb); + + std::string bothver = windowtit + " (" + std::string(basedof) + ")"; + Fl_Box *versionlabel = new Fl_Box(FL_NO_BOX, 620, 700, 160, 30, bothver.c_str()); + versionlabel->align(FL_ALIGN_RIGHT | FL_ALIGN_INSIDE); + + std::vector fpaths; + std::string rdir = Common::getbasedir(false); + scandir(rdir, rdir, fpaths); + updatelist(); + + window->end(); + window->show(argc, argv); + + return Fl::run(); +} diff --git a/main.hh b/main.hh new file mode 100644 index 0000000..fb9a252 --- /dev/null +++ b/main.hh @@ -0,0 +1,24 @@ +#ifndef MAIN_HH +#define MAIN_HH + +#include +#include + +extern std::vector fullpaths; +extern std::vector dispaths; +extern int browseId; +extern bool isPassHidden; +extern std::string realpass; + +class Fl_Text_Display; +class Fl_Text_Buffer; + +extern Fl_Text_Display *textview; +extern Fl_Text_Buffer *textbuf; + +void browse(std::string &path, bool isNew); +void clearpaths(bool isReset, std::string &path); +void updatelist(); +void scandir(const std::string& dir, const std::string& root, std::vector& paths); + +#endif diff --git a/simpas.desktop b/simpas.desktop new file mode 100644 index 0000000..e4d93c8 --- /dev/null +++ b/simpas.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=SimPas +GenericName=Simple Password Manager +GenericName[ja]=シンプルなパスワードマネージャー +Exec=simpas %F +Icon=/usr/local/share/icons/076/512x512/simpas +StartupNotify=true +Terminal=false +Type=Application +Categories=Application;Utility;System;Security;DeskopUtility; +Keywords=password;password manager;security;otp;password generator; diff --git a/src/addpass.cc b/src/addpass.cc new file mode 100644 index 0000000..b7f2d2f --- /dev/null +++ b/src/addpass.cc @@ -0,0 +1,307 @@ +#include "common.hh" +#include "addpass.hh" +#include "../main.hh" + +#include +#include +#include + +#undef None +#include +#include + +#include + +#include + +Addpass add; + +struct InputData { + Fl_Input *txtin; + Fl_Secret_Input *pass1; + Fl_Secret_Input *pass2; + Fl_Window *dialog; +}; + +bool Addpass::exec(const std::string &file, const std::string &pass, bool isEdit) { + std::string lang = Common::getlang(); + + std::string basedir = Common::getbasedir(true); + std::string ext = ".gpg"; + + std::string gpgoutfile = (isEdit ? file : basedir + file + ext); + + if (access(gpgoutfile.c_str(), F_OK) != -1) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Password already exist." : + "パスワードが既に存在しています。"); + fl_alert("%s", err.c_str()); + return false; + } + + try { + // GPGMEライブラリを設置 + std::setlocale(LC_ALL, ""); + GpgME::initializeLibrary(); + GpgME::setDefaultLocale(LC_CTYPE, std::setlocale(LC_CTYPE, NULL)); + + GpgME::Error err; + if (err) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to generate GPGME" : + "GPGエラーの設置に失敗"); + fl_alert("%s: %s", ero.c_str(), gpg_strerror(err.code())); + return false; + } + + // GPGMEを創作 + std::unique_ptr ctx = + GpgME::Context::create(GpgME::Protocol::OpenPGP); + + // GPGMEは非対話的モードに設定 + err = ctx->setPinentryMode(GpgME::Context::PinentryMode::PinentryLoopback); + if (err) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to set pinentry mode" : + "pinentryモードを設定に失敗"); + fl_alert("%s: %s", ero.c_str(), gpg_strerror(err.code())); + return false; + } + + // パスワードからデータオブジェクトを創作 + GpgME::Data in(pass.c_str(), strlen(pass.c_str()), false); + GpgME::Data out; + + // 鍵を受け取る + std::string keypath = basedir + ".gpg-id"; + std::ifstream keyfile(keypath, std::ios::binary); + if (!keyfile.is_open()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to open .gpg-id file" : + ".gpg-idファイルを開くに失敗"); + fl_alert("%s", ero.c_str()); + return false; + } + + std::string keyid; + std::string line; + while (std::getline(keyfile, line)) { + line.erase(line.find_last_not_of(" \n\r\t") + 1); + if (!line.empty()) keyid = line; + } + + keyfile.close(); + + if (keyid.empty()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "The .gpg-id file is empty or invalid" : + ".gpg-idファイルは空か無効です"); + fl_alert("%s", ero.c_str()); + return false; + } + + GpgME::Key key = ctx->key(keyid.c_str(), err); + if (key.isNull()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to get key" : + "鍵を受取に失敗"); + fl_alert("%s", ero.c_str()); + return false; + } + + // 暗号化 + std::vector keys = {key}; + + GpgME::EncryptionResult res = + ctx->encrypt(keys, in, out, GpgME::Context::AlwaysTrust); + if (res.error()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to encrypt" : + "暗号化に失敗"); + fl_alert("%s: %s", ero.c_str(), res.error().asString()); + return false; + } + + // ディレクトリを創作 + std::string dirpath = (isEdit ? file : basedir + file); + auto lastsla = dirpath.find_last_of('/'); + if (lastsla != std::string::npos) { + dirpath = dirpath.substr(0, lastsla); + + try { + Common common; + common.mkdir_r(dirpath, 0755); + } catch (const std::runtime_error &e) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to create directory" : + "ディレクトリを創作に失敗"); + fl_alert("%s: %s", ero.c_str(), e.what()); + return false; + } + } + + // 暗号化したファイルを開く + std::ofstream gpgpath(gpgoutfile, std::ios::binary); + if (!gpgpath.is_open()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to write file '" + gpgoutfile + "'." : + "「" + gpgoutfile + "」ファイルを書き込めません。"); + fl_alert("%s", ero.c_str()); + return false; + } + + // データが保存したかどうか確認 + ssize_t encrypted_data_size = out.seek(0, SEEK_END); + if (encrypted_data_size <= 0) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to store the data" : + "データを保存に失敗"); + fl_alert("%s", ero.c_str()); + return false; + } + + // 復号化したパスワードを表示する + out.seek(0, SEEK_SET); + + char buffer[512]; + ssize_t read_bytes; + while ((read_bytes = out.read(buffer, sizeof(buffer))) > 0) { + gpgpath.write(buffer, read_bytes); + } + + gpgpath.close(); + + if (gpgpath.fail()) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to write encrypted data to file" : + "暗号化データを書き込めません。"); + fl_alert("%s", ero.c_str()); + return false; + } + } catch (const std::exception &e) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Error" : + "エラー"); + fl_alert("%s: %s", err.c_str(), e.what()); + return false; + } + + if (isEdit) return true; + + std::string msg = (lang.compare(0, 2, "en") == 0 ? + "The password got saved." : + "パスワードを保存出来ました"); + fl_alert("%s", msg.c_str()); + + return true; +} + +void Addpass::add_cb(Fl_Widget *, void *data) { + InputData *inputs = (InputData *)data; + std::string lang = Common::getlang(); + + if (inputs) { + file = inputs->txtin->value(); + if (file.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the path." : + "パスをご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + inputpass1 = inputs->pass1->value(); + if (inputpass1.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the password." : + "パスワードをご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + inputpass2 = inputs->pass2->value(); + if (inputpass2.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the password (confirm)." : + "パスワード (確認)をご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + + if (inputpass1 != inputpass2) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Password does not match." : + "パスワードが一致していません。"); + fl_alert("%s", err.c_str()); + return; + } + + if (exec(file, inputpass1, false)) { + inputs->dialog->hide(); + } + } else { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in all the fields." : + "全てのフィールドをご入力下さい。"); + fl_alert("%s", err.c_str()); + } +} + +void Addpass::dialog_cb(Fl_Widget *w, void *) { + (void)w; + std::string lang = Common::getlang(); + Fl_Window *dialog = new Fl_Window(400, 200, + (lang.compare(0, 2, "en") == 0 ? "Add password" : "パスワードの追加")); + + Fl_Input *txtin = new Fl_Input(150, 20, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Path:" : "パス:")); + dialog->add(txtin); + + Fl_Secret_Input *pass1 = new Fl_Secret_Input(150, 60, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Password:" : "パスワード:")); + Fl_Secret_Input *pass2 = new Fl_Secret_Input(150, 100, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Password (confirm):" : "パスワード (確認):")); + dialog->add(pass1); + dialog->add(pass2); + + InputData *inputs = new InputData(); + inputs->txtin = txtin; + inputs->pass1 = pass1; + inputs->pass2 = pass2; + inputs->dialog = dialog; + + Fl_Button *okbtn = new Fl_Button(60, 150, 80, 30, "OK"); + Fl_Button *cancelbtn = new Fl_Button(160, 150, 80, 30, + (lang.compare(0, 2, "en") == 0 ? "Cancel" : "キャンセル")); + + okbtn->callback(static_ok_cb, inputs); + cancelbtn->callback(static_cancel_cb, dialog); + + dialog->add(okbtn); + dialog->add(cancelbtn); + + dialog->end(); + dialog->set_modal(); + dialog->show(); +} + +void Addpass::static_ok_cb(Fl_Widget *w, void *data) { + (void)w; + InputData *inputs = (InputData *)data; + + add.add_cb(nullptr, inputs); + std::vector fpaths; + std::string rdir = Common::getbasedir(false); + std::string curpath = rdir + "/" + inputs->txtin->value() + ".gpg"; + clearpaths(false, curpath); + scandir(rdir, rdir, fpaths); + updatelist(); + browse(curpath, true); +} + +void Addpass::static_cancel_cb(Fl_Widget *w, void *data) { + ((Addpass *)data)->cancel_cb(w, data); +} diff --git a/src/addpass.hh b/src/addpass.hh new file mode 100644 index 0000000..9cfd012 --- /dev/null +++ b/src/addpass.hh @@ -0,0 +1,28 @@ +#ifndef ADDPASS_HH +#define ADDPASS_HH + +#include +#include +#include + +#include "dialog.hh" + +#include + +class Addpass : public Dialog { + public: + Fl_Button *btn = nullptr; + std::string file; + std::string inputpass1; + std::string inputpass2; + + static void dialog_cb(Fl_Widget *w, void *); + void add_cb(Fl_Widget *, void *); + bool exec(const std::string &file, const std::string &pass, bool isEdit); + + private: + static void static_ok_cb(Fl_Widget *w, void *data); + static void static_cancel_cb(Fl_Widget *w, void *data); +}; + +#endif diff --git a/src/base32.cc b/src/base32.cc new file mode 100644 index 0000000..48625ef --- /dev/null +++ b/src/base32.cc @@ -0,0 +1,53 @@ +#include "base32.hh" + +#include +#include +#include +#include +#include +#include + +int Base32::char_to_val(char c) { + std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + auto it = std::find(alphabet.begin(), alphabet.end(), c); + return (it != alphabet.end()) ? std::distance(alphabet.begin(), it) : -1; +} + +std::vector Base32::decode(const std::string &encoded) { + std::string encoded_up = encoded; + for (auto &c : encoded_up) { + c = std::toupper(static_cast(c)); + } + + size_t encoded_len = encoded_up.length(); + size_t padding = 0; + for (int i = encoded_len - 1; i >= 0 && encoded_up[i] == '='; --i) { + ++padding; + } + + size_t out_len = (encoded_len - padding) * 5 / 8; + if (out_len == 0) return {}; + + std::vector decoded(out_len); + + int buffer = 0, bits_left = 0, count = 0; + Base32 b32; + for (size_t i = 0; i < encoded_len - padding; ++i) { + int val = b32.char_to_val(encoded_up[i]); + if (val < 0) { + throw std::runtime_error("Base32エンコードした文字の中に不正な文字があります。"); + } + + buffer <<= 5; + buffer |= val; + bits_left += 5; + + if (bits_left >= 8) { + decoded[count++] = static_cast(buffer >> (bits_left - 8)); + bits_left -= 8; + } + } + + decoded.resize(count); + return decoded; +} diff --git a/src/base32.hh b/src/base32.hh new file mode 100644 index 0000000..7830104 --- /dev/null +++ b/src/base32.hh @@ -0,0 +1,15 @@ +#ifndef BASE32_HH +#define BASE32_HH + +#include +#include + +class Base32 { + public: + static std::vector decode(const std::string &encoded); + + private: + int char_to_val(char c); +}; + +#endif diff --git a/src/chkpass.cc b/src/chkpass.cc new file mode 100644 index 0000000..6904da3 --- /dev/null +++ b/src/chkpass.cc @@ -0,0 +1,279 @@ +#include "common.hh" +#include "chkpass.hh" +#include "../main.hh" +#include "showpass.hh" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +Chkpass chk; +std::vector foundRes; +std::vector duppath; +std::vector duppasswd; + +struct InputData { + Fl_Radio_Button *all; + Fl_Radio_Button *len; + Fl_Radio_Button *chr; + Fl_Radio_Button *dup; + Fl_Window *dialog; +}; + +void Chkpass::lenPass(const std::string &path, const std::string &pass, + const std::string &lang) { + if (pass.length() > 0 && pass.length() < minimumlen) { + std::string res = "【E】"; + res += (lang.compare(0, 2, "en") == 0 ? + "The password \"" + path + "\" is too short, minimum length should be " + + std::to_string(minimumlen) + " characters, recommended is " + + std::to_string(recommendlen) + " characters.": + "パスワード「" + path + "」は短すぎます。最短パスワードの長さは" + + std::to_string(minimumlen) + "文字ですが、勧めが" + + std::to_string(recommendlen) + "文字です。"); + + foundRes.push_back(res); + weaklencount++; + vulncount++; + } else if (pass.length() >= minimumlen && pass.length() < recommendlen) { + std::string res = "【W】"; + res += (lang.compare(0, 2, "en") == 0 ? + "The password \"" + path + "\" is long enough, but for optimal security, " + + std::to_string(recommendlen) + " characters is recommended.\n" : + "パスワード「" + path + "」の長さは十分ですが、最強のセキュリティには" + + std::to_string(recommendlen) + "文字が勧めします。\n"); + + foundRes.push_back(res); + } +} + +void Chkpass::charPass(const std::string &path, const std::string &pass, + const std::string &lang) { + bool isUpper = false; + bool isLower = false; + bool isDigit = false; + bool isSpecial = false; + + for (char ch : pass) { + if (std::isupper(static_cast(ch))) isUpper = true; + if (std::islower(static_cast(ch))) isLower = true; + if (std::isdigit(static_cast(ch))) isDigit = true; + if (std::find(spchar.begin(), spchar.end(), ch) != spchar.end()) isSpecial = true; + } + + if (!isUpper || !isLower || !isDigit || !isSpecial) { + std::string res = "【E】"; + res += (lang.compare(0, 2, "en") == 0 ? + "The password \"" + path + "\" is too weak! A strong password contains " + + "at least 1 uppercase, 1 lowercase, 1 digit, and 1 special character." : + "パスワード「" + path + "」は弱すぎます!強いパスワードは最大1大文字、" + + "1小文字、1数字、及び1記号の文字が含みます。"); + + foundRes.push_back(res); + weakcharcount++; + vulncount++; + } +} + +void Chkpass::dupPass(const std::string &path, const std::string &pass, + const std::string &lang) { + for (std::size_t k = 0; k < duppasswd.size(); k++) { + if (pass.compare(duppasswd[k]) == 0) { + std::string res = "【E】"; + res += (lang.compare(0, 2, "en") == 0 ? + "The password \"" + path + "\" is the same as \"" + duppath[k] + "\". " + + "For security, please keep passwords unique!": + "パスワード「" + path + "」は「" + duppath[k] + "」と一致しています。" + + "セキュリティの為、各パスワードはユニークにする様にして下さい!"); + + foundRes.push_back(res); + duppasscount++; + vulncount++; + } + } + + duppath.push_back(path); + duppasswd.push_back(pass); +} + +bool Chkpass::exec() { + std::string lang = Common::getlang(); + + // パスワードをスキャンして + Showpass show; + + for (const auto &dispath : dispaths) { + std::string fullpath = Common::getbasedir(true) + dispath + ".gpg"; + std::string pass = show.exec(fullpath.c_str(), true); + if (pass.empty()) continue; + if (pass.rfind("otpauth://totp/", 0) == 0) continue; + + if (isAll) { + lenPass(dispath, pass, lang); + charPass(dispath, pass, lang); + dupPass(dispath, pass, lang); + } else if (isLen) { + lenPass(dispath, pass, lang); + } else if (isChar) { + charPass(dispath, pass, lang); + } else if (isDup) { + dupPass(dispath, pass, lang); + } + } + + return true; +} + +void Chkpass::showRes() { + std::string lang = Common::getlang(); + + std::string res; + if (lang.compare(0, 2, "en") == 0) { + res = "Weak passwords:\n"; + } else { + res = "不安定なパスワード:\n"; + } + + for (const auto &path : foundRes) { + res += path + "\n"; + } + + if (lang.compare(0, 2, "en") == 0) { + res += "Short password count: " + std::to_string(weaklencount) + "\n"; + res += "Weak password count: " + std::to_string(weakcharcount) + "\n"; + res += "Duplicate password count: " + std::to_string(duppasscount) + "\n"; + res += "Total: " + std::to_string(duppath.size()) + "\n"; + res += "It's advised to change any of the"; + res += "weak passwords as soon as possible!"; + } else { + res += "短いパスワード数: " + std::to_string(weaklencount) + "\n"; + res += "弱いパスワード数: " + std::to_string(weakcharcount) + "\n"; + res += "同じパスワード数: " + std::to_string(duppasscount) + "\n"; + res += "合計: " + std::to_string(duppath.size()) + "\n"; + res += "不安定なパスワードは出来るだけ早く変更する事をお勧めします!"; + } + + Fl_Window *win = new Fl_Window(500, 440, lang.compare(0, 2, "en") == 0 ? + "Results" : "結果"); + Fl_Text_Display *display = new Fl_Text_Display(10, 10, 480, 380); + Fl_Text_Buffer *textbuf = new Fl_Text_Buffer(); + + textbuf->text(res.c_str()); + display->buffer(textbuf); + display->scrollbar_width(15); + win->resizable(display); + + Fl_Button *okBtn = new Fl_Button(210, 400, 80, 30, "OK"); + okBtn->callback([](Fl_Widget *widget, void *win) { + (void)widget; + reinterpret_cast(win)->hide(); + }, win); + + win->add(okBtn); + + win->end(); + win->show(); +} + +void Chkpass::chk_cb(Fl_Widget *, void *data) { + InputData *inputs = (InputData *)data; + std::string lang = Common::getlang(); + + if (inputs) { + isAll = inputs->all->value(); + isLen = inputs->len->value(); + isChar = inputs->chr->value(); + isDup = inputs->dup->value(); + } + + Fl_Window *dialog = new Fl_Window(400, 50, + (lang.compare(0, 2, "en") == 0 ? + "Checking for weak password" : "不安的なパスワードの確認中")); + + Fl_Box *box = new Fl_Box(10, 10, 380, 20, + (lang.compare(0, 2, "en") == 0 ? + "Checking, please wait for a while..." : + "確認中。暫くお待ち下さい・・・")); + + dialog->add(box); + + dialog->end(); + dialog->set_modal(); + dialog->show(); + + std::thread checker([dialog]() { + chk.exec(); + + Fl::lock(); + dialog->hide(); + chk.showRes(); + Fl::unlock(); + }); + + checker.detach(); +} + +void Chkpass::dialog_cb(Fl_Widget *w, void *) { + (void)w; + + std::string lang = Common::getlang(); + + Fl_Window *dialog = new Fl_Window(390, 145, + (lang.compare(0, 2, "en") == 0 ? + "Check for weak password" : "不安的なパスワードの確認")); + + Fl_Radio_Button *all = new Fl_Radio_Button(10, 10, 180, 30, + (lang.compare(0, 2, "en") == 0) ? "Everything" : "全部"); + Fl_Radio_Button *len = new Fl_Radio_Button(10, 50, 180, 30, + (lang.compare(0, 2, "en") == 0) ? "Length" : "長さ"); + Fl_Radio_Button *chr = new Fl_Radio_Button(200, 10, 180, 30, + (lang.compare(0, 2, "en") == 0) ? "Strength" : "強さ"); + Fl_Radio_Button *dup = new Fl_Radio_Button(200, 50, 180, 30, + (lang.compare(0, 2, "en") == 0) ? "Duplicate" : "服数度"); + + dialog->add(all); + dialog->add(len); + dialog->add(chr); + dialog->add(dup); + + InputData *inputs = new InputData(); + inputs->all = all; + inputs->len = len; + inputs->chr = chr; + inputs->dup = dup; + inputs->dialog = dialog; + + Fl_Button *okbtn = new Fl_Button(105, 100, 80, 30, "OK"); + Fl_Button *cancelbtn = new Fl_Button(205, 100, 80, 30, + (lang.compare(0, 2, "en") == 0 ? "Cancel" : "キャンセル")); + + okbtn->callback(static_ok_cb, inputs); + cancelbtn->callback(static_cancel_cb, dialog); + + dialog->add(okbtn); + dialog->add(cancelbtn); + + dialog->end(); + dialog->set_modal(); + dialog->show(); +} + +void Chkpass::static_ok_cb(Fl_Widget *w, void *data) { + (void)w; + InputData *inputs = (InputData *)data; + + chk.chk_cb(nullptr, inputs); +} + +void Chkpass::static_cancel_cb(Fl_Widget *w, void *data) { + ((Chkpass *)data)->cancel_cb(w, data); +} diff --git a/src/chkpass.hh b/src/chkpass.hh new file mode 100644 index 0000000..7d12911 --- /dev/null +++ b/src/chkpass.hh @@ -0,0 +1,45 @@ +#ifndef CHKPASS_HH +#define CHKPASS_HH + +#include +#include + +#include "dialog.hh" + +#include + +class Chkpass : public Dialog { + public: + Fl_Button *btn = nullptr; + Fl_Radio_Button *allChk = nullptr; + Fl_Radio_Button *lenChk = nullptr; + Fl_Radio_Button *charChk = nullptr; + Fl_Radio_Button *dupChk = nullptr; + bool isAll, isLen, isChar, isDup = false; + + static void dialog_cb(Fl_Widget *w, void *); + void showRes(); + void chk_cb(Fl_Widget *, void *); + bool exec(); + + private: + int vulncount = 0; + int weaklencount = 0; + int weakcharcount = 0; + int duppasscount = 0; + + std::size_t minimumlen = 12; + std::size_t recommendlen = 64; + + std::string spchar = "!@#$%^&*()-_=+[]{}|;:'\",.<>?/\\`~"; + + void lenPass(const std::string &path, const std::string &pass, + const std::string &lang); + void charPass(const std::string &path, const std::string &pass, + const std::string &lang); + void dupPass(const std::string &path, const std::string &pass, + const std::string &lang); + static void static_ok_cb(Fl_Widget *w, void *data); + static void static_cancel_cb(Fl_Widget *w, void *data); +}; +#endif diff --git a/src/common.cc b/src/common.cc new file mode 100644 index 0000000..98a97f9 --- /dev/null +++ b/src/common.cc @@ -0,0 +1,82 @@ +#include "common.hh" + +#include +#include +#include + +#include +#include +#include + +std::string Common::getbasedir(bool trailing) { + std::string homedir = std::getenv("HOME") ? std::getenv("HOME") : ""; + if (homedir.empty()) return ""; + +#if defined(__HAIKU__) + std::string basedir = "/config/settings/sp"; + std::string slash = "/"; +#elif defined(_WIN32) + std::string basedir = "\\AppData\\Local\\076\\sp"; + std::string slash = "\\"; +#else + std::string basedir = "/.local/share/sp"; + std::string slash = "/"; +#endif + + return trailing ? (homedir + basedir + slash) : (homedir + basedir); +} + +std::string Common::getlang() { + const char *env = std::getenv("SP_LANG"); + std::string lang; + + if (env) lang = env; + else lang = "ja"; + + return lang; +} + +int Common::mkdir_r(const std::string &path, mode_t mode) { + char tmp[256]; + size_t len; + + std::snprintf(tmp, sizeof(tmp), "%s", path.c_str()); + + len = std::strlen(tmp); + if (tmp[len - 1] == '/') { + tmp[len - 1] = 0; + } + + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = 0; + if (mkdir(tmp, mode) != 0 && errno != EEXIST) return -1; + *p = '/'; + } + } + + if (mkdir(tmp, mode) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void Common::tmpcopy(const std::string &inpath, const std::string &outpath) { + std::ifstream src(inpath, std::ios::binary); + std::ofstream dst(outpath, std::ios::binary); +} + +std::vector Common::explode(const std::string &str, char delimiter) { + std::vector tokens; + std::string token; + size_t start = 0, end = 0; + + while ((end = str.find(delimiter, start)) != std::string::npos) { + tokens.push_back(str.substr(start, end - start)); + start = end + 1; + } + tokens.push_back(str.substr(start)); + + return tokens; +} diff --git a/src/common.hh b/src/common.hh new file mode 100644 index 0000000..69a00eb --- /dev/null +++ b/src/common.hh @@ -0,0 +1,18 @@ +#ifndef COMMON_HH +#define COMMON_HH + +#include +#include + +#include + +class Common { + public: + static std::string getbasedir(bool trailing); + static std::string getlang(); + int mkdir_r(const std::string &path, mode_t mode); + void tmpcopy(const std::string &inpath, const std::string &outpath); + static std::vector explode(const std::string &str, char delimiter); +}; + +#endif diff --git a/src/delpass.cc b/src/delpass.cc new file mode 100644 index 0000000..e848ec1 --- /dev/null +++ b/src/delpass.cc @@ -0,0 +1,147 @@ +#include "common.hh" +#include "delpass.hh" +#include "../main.hh" + +#include +#include + +#include + +Delpass del; + +struct InputData { + Fl_Input *txtin; + Fl_Window *dialog; +}; + +void Delpass::setFile(std::string &f) { + del.file = f; +} + +std::string Delpass::getFile() { + return file; +} + +bool Delpass::exec(const std::string &file, bool force) { + std::string lang = Common::getlang(); + + std::string basedir = Common::getbasedir(true); + std::string ext = ".gpg"; + + // ファイルが既に存在するかどうか確認 + if (access(file.c_str(), F_OK) != 0) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Password does not exist" : + "パスワードが存在しません"); + fl_alert("%s", err.c_str()); + return false; + } + + if (unlink(file.c_str()) == -1) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Password cannot be deleted" : + "パスワードを削除出来ませんですた"); + fl_alert("%s", err.c_str()); + return false; + } + + // 空のディレクトリの場合 + std::vector tokens = Common::explode(file, '/'); + std::string passpath = basedir + tokens[0]; + + for (size_t i = 1; i < tokens.size(); ++i) { + if (i == tokens.size() - 1) continue; + passpath += "/" + tokens[i]; + } + + for (int i = tokens.size() - 1; i >= 0; --i) { + // ~/.local/share/sp を削除したら危険 + if (passpath.compare(0, basedir.size(), basedir) == 0) { + break; + } + + // ディレクトリが空じゃない場合、削除を止める + if (rmdir(passpath.c_str()) == -1) { + break; + } + + size_t last_slash = passpath.find_last_of('/'); + if (last_slash != std::string::npos) { + passpath.erase(last_slash); + } + } + + if (force) return true; + + std::string msg = (lang.compare(0, 2, "en") == 0 ? + "The password got deleted" : + "パスワードを削除しました"); + fl_alert("%s", msg.c_str()); + return true; +} + +void Delpass::delete_cb(Fl_Widget *, void *data) { + InputData *inputs = (InputData *)data; + + if (inputs) { + file = inputs->txtin->value(); + } + + exec(file, false); + std::vector fpaths; + std::string rdir = Common::getbasedir(false); + + std::string mockpath = ""; + clearpaths(true, mockpath); + scandir(rdir, rdir, fpaths); + updatelist(); +} + +void Delpass::dialog_cb(Fl_Widget *w, void *data) { + (void)w; + (void)data; + std::string lang = Common::getlang(); + + Fl_Input *txtin = new Fl_Input(150, 20, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Path:" : "パス:")); + txtin->hide(); + txtin->value(del.getFile().c_str()); + + InputData *inputs = new InputData(); + inputs->txtin = txtin; + + std::string asking = + (lang.compare(0, 2, "en") == 0 ? + "Are you sure you want to delete the password '" + del.getFile() + "'?" : + "パスワード「" + del.getFile() + "」を本当に削除する事が宜しいでしょうか?"); + int confirm = fl_choice("%s", + (lang.compare(0, 2, "en") == 0 ? "Cancel" : "キャンセル"), + (lang.compare(0, 2, "en") == 0 ? "Delete" : "削除"), + nullptr, asking.c_str()); + + if (confirm == 0) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Not deleted" : + "削除しませんでした"); + fl_alert("%s", err.c_str()); + } else { + static_ok_cb(w, inputs); + } +} + +void Delpass::static_ok_cb(Fl_Widget *w, void *data) { + (void)w; + InputData *inputs = (InputData *)data; + + del.delete_cb(nullptr, inputs); + std::vector fpaths; + std::string rdir = Common::getbasedir(false); + std::string curpath = ""; + clearpaths(true, curpath); + scandir(rdir, rdir, fpaths); + updatelist(); +} + +void Delpass::static_cancel_cb(Fl_Widget *w, void *data) { + ((Delpass *)data)->cancel_cb(w, data); +} diff --git a/src/delpass.hh b/src/delpass.hh new file mode 100644 index 0000000..d8f67ac --- /dev/null +++ b/src/delpass.hh @@ -0,0 +1,25 @@ +#ifndef DELPASS_HH +#define DELPASS_HH + +#include +#include + +#include "dialog.hh" + +class Delpass : public Dialog { + public: + Fl_Button *btn = nullptr; + std::string file; + + static void dialog_cb(Fl_Widget *w, void *); + void delete_cb(Fl_Widget *, void *data); + static void setFile(std::string &f); + std::string getFile(); + bool exec(const std::string &file, bool force); + + private: + static void static_ok_cb(Fl_Widget *w, void *data); + static void static_cancel_cb(Fl_Widget *w, void *data); +}; + +#endif diff --git a/src/dialog.cc b/src/dialog.cc new file mode 100644 index 0000000..c1618ae --- /dev/null +++ b/src/dialog.cc @@ -0,0 +1,13 @@ +#include "dialog.hh" + +#include + +void Dialog::ok_cb(Fl_Widget *w, void *dialog) { + (void)w; + ((Fl_Window *)dialog)->hide(); +} + +void Dialog::cancel_cb(Fl_Widget *w, void *dialog) { + (void)w; + ((Fl_Window *)dialog)->hide(); +} diff --git a/src/dialog.hh b/src/dialog.hh new file mode 100644 index 0000000..c40ce16 --- /dev/null +++ b/src/dialog.hh @@ -0,0 +1,12 @@ +#ifndef DIALOG_HH +#define DIALOG_HH + +#include + +class Dialog { + public: + static void ok_cb(Fl_Widget *w, void *dialog); + static void cancel_cb(Fl_Widget *w, void *dialog); +}; + +#endif diff --git a/src/editpass.cc b/src/editpass.cc new file mode 100644 index 0000000..4978ae1 --- /dev/null +++ b/src/editpass.cc @@ -0,0 +1,189 @@ +#include "common.hh" +#include "editpass.hh" +#include "delpass.hh" +#include "addpass.hh" +#include "../main.hh" + +#include +#include + +#include +#include +#include + +Editpass edit; + +struct InputData { + Fl_Input *txtin; + Fl_Secret_Input *pass1; + Fl_Secret_Input *pass2; + Fl_Window *dialog; +}; + +void Editpass::setFile(std::string &f) { + edit.file = f; +} + +std::string Editpass::getFile() { + return file; +} + +bool Editpass::exec(const std::string &file, const std::string &pass) { + std::string lang = Common::getlang(); + + Common c; + c.tmpcopy(file, "/tmp/simpas-tmp.gpg"); + + Delpass d; + bool isdel = d.exec(file, true); + if (!isdel) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? "Failed to edit." : "編集に失敗。"); + fl_alert("%s", err.c_str()); + return false; + } + + Addpass a; + bool isadd = a.exec(file, pass, true); + if (!isadd) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? "Failed to edit." : "編集に失敗。"); + fl_alert("%s", err.c_str()); + // TODO: うまく動いているかの確認。それは次のバージョンから・・・ + /* std::vector tokens = Common::explode(file, '/'); */ + /* std::string passpath = tokens[0]; */ + + /* for (size_t i = 1; i < tokens.size(); ++i) { */ + /* if (i == tokens.size() - 1) continue; */ + /* passpath += "/" + tokens[i]; */ + /* } */ + + /* for (int i = tokens.size() - 1; i >= 0; ++i) { */ + /* if (c.mkdir_r(passpath, 0755) == -1) break; */ + + /* size_t last_slash = passpath.find_last_of('/'); */ + /* if (last_slash != std::string::npos) passpath.erase(last_slash); */ + /* } */ + c.tmpcopy("/tmp/simpas-tmp.gpg", file); + unlink("/tmp/simpas-tmp.gpg"); + return false; + } + + std::string msg = + (lang.compare(0, 2, "en") == 0 ? "Edit success." : "編集に成功。"); + fl_alert("%s", msg.c_str()); + unlink("/tmp/simpas-tmp.gpg"); + + return true; +} + +void Editpass::edit_cb(Fl_Widget *, void *data) { + InputData *inputs = (InputData *)data; + std::string lang = Common::getlang(); + + if (inputs) { + file = inputs->txtin->value(); + if (file.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the path." : + "パスをご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + inputpass1 = inputs->pass1->value(); + if (inputpass1.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the password." : + "パスワードをご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + inputpass2 = inputs->pass2->value(); + if (inputpass2.empty()) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in the password (confirm)." : + "パスワード (確認)をご入力下さい。"); + fl_alert("%s", err.c_str()); + return; + } + + if (inputpass1 != inputpass2) { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Password does not match." : + "パスワードが一致していません。"); + fl_alert("%s", err.c_str()); + return; + } + + if (exec(file, inputpass1)) { + inputs->dialog->hide(); + } + } else { + std::string err = + (lang.compare(0, 2, "en") == 0 ? + "Please fill in all the fields." : + "全てのフィールドをご入力下さい。"); + fl_alert("%s", err.c_str()); + } +} + +void Editpass::dialog_cb(Fl_Widget *w, void *data) { + (void)w; + (void)data; + std::string lang = Common::getlang(); + Fl_Window *dialog = new Fl_Window(400, 160, + (lang.compare(0, 2, "en") == 0 ? "Edit password" : "パスワードの編集")); + + Fl_Input *txtin = new Fl_Input(150, 20, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Path:" : "パス:")); + Fl_Secret_Input *pass1 = new Fl_Secret_Input(150, 20, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Password:" : "パスワード:")); + Fl_Secret_Input *pass2 = new Fl_Secret_Input(150, 60, 180, 30, + (lang.compare(0, 2, "en") == 0 ? "Password (confirm):" : "パスワード (確認):")); + dialog->add(pass1); + dialog->add(pass2); + txtin->hide(); + txtin->value(edit.getFile().c_str()); + + InputData *inputs = new InputData(); + inputs->txtin = txtin; + inputs->pass1 = pass1; + inputs->pass2 = pass2; + inputs->dialog = dialog; + + Fl_Button *okbtn = new Fl_Button(60, 110, 80, 30, "OK"); + Fl_Button *cancelbtn = new Fl_Button(160, 110, 80, 30, + (lang.compare(0, 2, "en") == 0 ? "Cancel" : "キャンセル")); + + okbtn->callback(static_ok_cb, inputs); + cancelbtn->callback(static_cancel_cb, dialog); + + dialog->add(okbtn); + dialog->add(cancelbtn); + + dialog->end(); + dialog->set_modal(); + dialog->show(); +} + +void Editpass::static_ok_cb(Fl_Widget *w, void *data) { + (void)w; + InputData *inputs = (InputData *)data; + + edit.edit_cb(nullptr, inputs); + std::vector fpaths; + std::string rdir = Common::getbasedir(false); + std::string curpath = inputs->txtin->value(); + clearpaths(false, curpath); + scandir(rdir, rdir, fpaths); + updatelist(); + browse(curpath, false); +} + +void Editpass::static_cancel_cb(Fl_Widget *w, void *data) { + ((Editpass *)data)->cancel_cb(w, data); +} diff --git a/src/editpass.hh b/src/editpass.hh new file mode 100644 index 0000000..8c7b24f --- /dev/null +++ b/src/editpass.hh @@ -0,0 +1,30 @@ +#ifndef EDITPASS_HH +#define EDITPASS_HH + +#include +#include +#include + +#include "dialog.hh" + +#include + +class Editpass : public Dialog { + public: + Fl_Button *btn = nullptr; + std::string file; + std::string inputpass1; + std::string inputpass2; + + static void dialog_cb(Fl_Widget *w, void *); + void edit_cb(Fl_Widget *, void *data); + static void setFile(std::string &f); + std::string getFile(); + bool exec(const std::string &file, const std::string &pass); + + private: + static void static_ok_cb(Fl_Widget *w, void *data); + static void static_cancel_cb(Fl_Widget *w, void *data); +}; + +#endif diff --git a/src/genpass.cc b/src/genpass.cc new file mode 100644 index 0000000..1e95707 --- /dev/null +++ b/src/genpass.cc @@ -0,0 +1,108 @@ +#include "common.hh" +#include "genpass.hh" + +#include +#include + +#include +#include + +Genpass gen; + +std::string Genpass::exec(int count, bool issecure) { + std::string lang = Common::getlang(); + + const std::string charset_risky = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + const std::string charset_secure = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()=~-^\\|_@`[{]};:+*<>,./?"; + const std::string &charset = issecure ? charset_secure : charset_risky; + + std::ifstream fp("/dev/random", std::ios_base::binary); + if (!fp.is_open()) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Could not open /dev/random" : + "/dev/randomを開けられませんでした"); + fl_alert("%s", err.c_str()); + return ""; + } + + std::vector password(count + 1); + for (int i = 0; i < count; ++i) { + unsigned char key; + fp.read(reinterpret_cast(&key), sizeof(key)); + if (!fp) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Could not read /dev/random" : + "/dev/randomから読み込みに失敗"); + fl_alert("%s", err.c_str()); + fp.close(); + std::exit(EXIT_FAILURE); + } + + password[i] = charset[key % charset.size()]; + } + + password[count] = '\0'; + fp.close(); + + return std::string(password.data()); +} + +void Genpass::generate_cb(Fl_Widget *, void *) { + int count = std::stoi(gen.counter->value()); + bool issecure = gen.securechk->value() > 0; + + std::string password = gen.exec(count, issecure); + gen.res->value(password.c_str()); +} + +void Genpass::dialog_cb(Fl_Widget *w, void *) { + (void)w; + std::string lang = Common::getlang(); + Fl_Window *dialog = new Fl_Window(450, 250, + (lang.compare(0, 2, "en") == 0 ? "Generate password" : "パスワードの作成")); + + gen.counter = new Fl_Input(120, 20, 100, 30, + (lang.compare(0, 2, "en") == 0 ? "Length:" : "長さ:")); + gen.counter->type(FL_INT_INPUT); + gen.counter->value("64"); + dialog->add(gen.counter); + + gen.securechk = new Fl_Check_Button(120, 70, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Secure?" : "安全化?")); + gen.securechk->value(1); + dialog->add(gen.securechk); + + gen.genbtn = new Fl_Button(120, 110, 150, 30, + (lang.compare(0, 2, "en") == 0 ? "Generate" : "作成")); + gen.genbtn->callback(generate_cb); + dialog->add(gen.genbtn); + + gen.res = new Fl_Output(120, 150, 300, 30, + (lang.compare(0, 2, "en") == 0 ? "Password:" : "パスワード:")); + gen.res->value(""); + dialog->add(gen.res); + + Fl_Button *okbtn = new Fl_Button(60, 200, 80, 30, "OK"); + Fl_Button *cancelbtn = new Fl_Button(160, 200, 80, 30, + (lang.compare(0, 2, "en") == 0 ? "Cancel" : "キャンセル")); + + okbtn->callback(static_ok_cb, dialog); + cancelbtn->callback(static_cancel_cb, dialog); + + dialog->add(okbtn); + dialog->add(cancelbtn); + + dialog->end(); + dialog->set_modal(); + dialog->show(); +} + +void Genpass::static_ok_cb(Fl_Widget *w, void *data) { + ((Genpass *)data)->ok_cb(w, data); +} + +void Genpass::static_cancel_cb(Fl_Widget *w, void *data) { + ((Genpass *)data)->cancel_cb(w, data); +} diff --git a/src/genpass.hh b/src/genpass.hh new file mode 100644 index 0000000..9000aa7 --- /dev/null +++ b/src/genpass.hh @@ -0,0 +1,33 @@ +#ifndef GENPASS_HH +#define GENPASS_HH + +#include +#include +#include +#include +#include +#include + +#include + +#include "dialog.hh" + +class Genpass : public Dialog { + public: + Fl_Button *btn = nullptr; + + std::string exec(int count, bool issecure); + static void dialog_cb(Fl_Widget *w, void *); + static void generate_cb(Fl_Widget *, void *data); + + private: + Fl_Input *counter = nullptr; + Fl_Check_Button *securechk = nullptr; + Fl_Button *genbtn = nullptr; + Fl_Output *res = nullptr; + + static void static_ok_cb(Fl_Widget *w, void *data); + static void static_cancel_cb(Fl_Widget *w, void *data); +}; + +#endif diff --git a/src/initpass.cc b/src/initpass.cc new file mode 100644 index 0000000..d7479c7 --- /dev/null +++ b/src/initpass.cc @@ -0,0 +1,58 @@ +#include "common.hh" +#include "initpass.hh" + +#include +#include + +#include +#include +#include +#include + +#include + +Initpass init; + +void Initpass::exec(const std::string &gpgid) { + std::string lang = Common::getlang(); + std::string basedir = Common::getbasedir(true); + + Common common; + if (common.mkdir_r(basedir, 0755) != 0 && errno != EEXIST) { + std::cout << basedir << std::endl; + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Failed to create directory." : + "ディレクトリを作成に失敗。"); + fl_alert("%s", err.c_str()); + return; + } + + std::string gpgidpath = basedir + "/.gpg-id"; + + struct stat statbuf; + if (stat(gpgidpath.c_str(), &statbuf) == 0) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + ".gpg-id file already exists." : + ".gpg-idファイルは既に存在します。"); + fl_alert("%s", err.c_str()); + return; + } + + std::ofstream gpgidfile(gpgidpath); + if (!gpgidfile.is_open()) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Failed to write .gpg-id file." : + ".gpg-idファイルを書き込めません。"); + fl_alert("%s", err.c_str()); + return; + } + + gpgidfile << gpgid + '\n'; + + gpgidfile.close(); + + std::string msg = (lang.compare(0, 2, "en") == 0 ? + "Initialization completed." : + "初期設定に完了しました。"); + fl_alert("%s", msg.c_str()); +} diff --git a/src/initpass.hh b/src/initpass.hh new file mode 100644 index 0000000..95519b3 --- /dev/null +++ b/src/initpass.hh @@ -0,0 +1,19 @@ +#ifndef INITPASS_HH +#define INITPASS_HH + +#include +#include + +#include + +#include "dialog.hh" + +class Initpass : public Dialog { + public: + Fl_Button *btn = nullptr; + Fl_Input *gpgid = nullptr; + + void exec(const std::string &gpgid); +}; + +#endif diff --git a/src/otppass.cc b/src/otppass.cc new file mode 100644 index 0000000..f871574 --- /dev/null +++ b/src/otppass.cc @@ -0,0 +1,119 @@ +#include "common.hh" +#include "base32.hh" +#include "otppass.hh" + +#include +#include + +#include + +#if defined(__APPLE) +#include +#define htobe64(x) OSSwapHostToBigInt64(x) +#endif + +std::vector Otppass::extract_secret(const std::string &otpauth_url) { + std::string lang = Common::getlang(); + + auto secret_start = otpauth_url.find("secret="); + if (secret_start == std::string::npos) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Failed to find secret in the OTPAuth URL" : + "OTPAuth URLの中に、シークレットを見つけられませんでした"); + fl_alert("%s", err.c_str()); + return {}; + } + secret_start += 7; + + auto secret_end = otpauth_url.find('&', secret_start); + if (secret_end == std::string::npos) { + secret_end = otpauth_url.length(); + } + + if (secret_end < secret_start) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Incorrect secret range" : + "不正なシークレットの距離"); + fl_alert("%s", err.c_str()); + return {}; + } + + std::string secret_encoded = + otpauth_url.substr(secret_start, secret_end - secret_start); + + std::vector secret_decoded; + secret_decoded = Base32::decode(secret_encoded.c_str()); + + if (secret_decoded.empty()) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Failed to decrypt BASE32" : + "BASE32の復号化に失敗"); + fl_alert("%s", err.c_str()); + return {}; + } + + return secret_decoded; +} + +#if defined(__HAIKU__) || defined(__linux) +uint64_t Otppass::htobe64(uint64_t counter) { + uint64_t res = 0; + uint8_t *dest = reinterpret_cast(&res); + const uint8_t *src = reinterpret_cast(&counter); + + for (int i = 0; i < 8; ++i) { + dest[i] = src[7 - i]; + } + + return res; +} +#endif + +uint32_t Otppass::generate_totp(const std::vector &secret, + uint64_t counter) { + counter = htobe64(counter); + + unsigned char hash[SHA_DIGEST_LENGTH]; + HMAC( + EVP_sha1(), + secret.data(), + secret.size(), + reinterpret_cast(&counter), + sizeof(counter), + hash, + NULL + ); + + int offset = hash[SHA_DIGEST_LENGTH - 1] & 0x0F; + uint32_t truncated_hash = + (hash[offset] & 0x7F) << 24 | + (hash[offset + 1] & 0xFF) << 16 | + (hash[offset + 2] & 0xFF) << 8 | + (hash[offset + 3] & 0xFF); + + return truncated_hash % 1000000; +} + +std::string Otppass::exec(std::string &secret) { + std::string lang = Common::getlang(); + + try { + std::vector secret_decoded = extract_secret(secret); + + time_t current_time = time(nullptr); + uint64_t counter = current_time / 30; + uint32_t otp = + generate_totp(secret_decoded, counter); + + char otpres[7]; + std::snprintf(otpres, sizeof(otpres), "%06u", otp); + + return std::string(otpres); + } catch (const std::exception &e) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Error" : + "エラー"); + fl_alert("%s: %s", err.c_str(), e.what()); + return ""; + } +} diff --git a/src/otppass.hh b/src/otppass.hh new file mode 100644 index 0000000..52aaaa0 --- /dev/null +++ b/src/otppass.hh @@ -0,0 +1,21 @@ +#ifndef OTPPASS_HH +#define OTPPASS_HH + +#include +#include +#include + +class Otppass { + public: + std::string exec(std::string &file); + + private: + std::vector extract_secret(const std::string &otpauth_url); +#if defined(__HAIKU__) || defined(__linux) + uint64_t htobe64(uint64_t counter); +#endif + uint32_t generate_totp(const std::vector &secret, + uint64_t counter); +}; + +#endif diff --git a/src/showpass.cc b/src/showpass.cc new file mode 100644 index 0000000..1c20280 --- /dev/null +++ b/src/showpass.cc @@ -0,0 +1,129 @@ +#include "common.hh" +#include "showpass.hh" +#include "../main.hh" +#include "otppass.hh" + +#include + +#undef Status +#undef None +#include +#include +#include +#include + +void Showpass::otpupdate_cb(void *o) { + Showpass *show = static_cast(o); + Otppass otp; + + if (!show->otpSav.empty()) { + std::string dec = otp.exec(show->otpSav); + + realpass = dec; + if (!isPassHidden) { + textbuf->text(dec.c_str()); + textview->redraw(); + } + } + + Fl::repeat_timeout(1.0, otpupdate_cb, o); +} + +std::string Showpass::exec(const char *file, bool stfu) { + std::string lang = Common::getlang(); + + try { + std::setlocale(LC_ALL, ""); + GpgME::initializeLibrary(); + + gpg_error_t err = gpg_err_init(); + if (err) { + if (!stfu) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to generate GPGME" : + "GPGエラーの設置に失敗"); + fl_alert("%s: %s", ero.c_str(), gpg_strerror(err)); + } + return ""; + } + + std::unique_ptr ctx = + GpgME::Context::create(GpgME::Protocol::OpenPGP); + if (!ctx) { + if (!stfu) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to create GPGME context" : + "GPGMEコンテキストの作成に失敗"); + fl_alert("%s", ero.c_str()); + } + return ""; + } + + if (ctx->protocol() != GpgME::Protocol::OpenPGP) { + if (!stfu) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to set OpenPGP protocol" : + "OpenPGPプロトコールの設置に失敗"); + fl_alert("%s", ero.c_str()); + } + return ""; + } + + std::ifstream gpgfile(file, std::ios::binary); + if (!gpgfile.is_open()) { + if (!stfu) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Unable to open the specified file" : + "指定されたファイルを開けません"); + fl_alert("%s", ero.c_str()); + } + return ""; + } + + GpgME::Data in(file); + GpgME::Data out; + + GpgME::DecryptionResult res = ctx->decrypt(in, out); + if (res.error()) { + if (!stfu) { + std::string ero = (lang.compare(0, 2, "en") == 0 ? + "Failed to decrypt" : + "復号化に失敗"); + fl_alert("%s: %s", ero.c_str(), res.error().asString()); + } + return ""; + } + + std::string dec; + char buf[512]; + ssize_t read_bytes; + + out.seek(0, SEEK_SET); + + while ((read_bytes = out.read(buf, sizeof(buf))) > 0) { + dec.append(buf, read_bytes); + } + + if (dec.rfind("otpauth://", 0) == 0 && !stfu) { + Otppass o; + otpSav = dec; + + Fl::remove_timeout(otpupdate_cb, this); + Fl::add_timeout(1.0, otpupdate_cb, this); + + return o.exec(dec); + } + + Fl::remove_timeout(otpupdate_cb, this); + otpSav = ""; + return dec; + } catch (const std::exception &e) { + if (!stfu) { + std::string err = (lang.compare(0, 2, "en") == 0 ? + "Error" : + "エラー"); + fl_alert("%s: %s", err.c_str(), e.what()); + } + return ""; + } +} diff --git a/src/showpass.hh b/src/showpass.hh new file mode 100644 index 0000000..8cf4bcd --- /dev/null +++ b/src/showpass.hh @@ -0,0 +1,21 @@ +#ifndef SHOWPASS_HH +#define SHOWPASS_HH + +#include +#include + +#include + +class Showpass { + public: + std::string otpSav = ""; + + static void otpupdate_cb(void *o); + std::string exec(const char *file, bool stfu); + + private: + void clean_up(GpgME::Context &ctx, GpgME::Data &in, GpgME::Data &out, + std::ifstream &gpgfile); +}; + +#endif diff --git a/src/vulnpass.cc b/src/vulnpass.cc new file mode 100644 index 0000000..d4d6a08 --- /dev/null +++ b/src/vulnpass.cc @@ -0,0 +1,198 @@ +#include "common.hh" +#include "vulnpass.hh" +#include "showpass.hh" +#include "../main.hh" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +Vulnpass vuln; +int vulncount = 0; +std::vector vulnpaths; + +bool Vulnpass::exec() { + std::string lang = Common::getlang(); + + // pwndサーバに接続 + int sock; + struct sockaddr_in srv; + struct addrinfo hints, *addr; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + int status = getaddrinfo("076.moe", nullptr, &hints, &addr); + if (status != 0) { + fl_alert("getaddrinfo: %s", gai_strerror(status)); + return false; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to create socket" : "ソケットを作成に失敗"); + return false; + } + + srv.sin_addr = ((struct sockaddr_in *)(addr->ai_addr))->sin_addr; + srv.sin_family = AF_INET; + srv.sin_port = htons(9951); + + freeaddrinfo(addr); + + if (connect(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to connect" : "接続に失敗"); + close(sock); + return false; + } + + // パスワードをスキャンして + Showpass show; + + for (const auto &dispath : dispaths) { + std::string fullpath = Common::getbasedir(true) + dispath + ".gpg"; + std::string pass = show.exec(fullpath.c_str(), true); + if (pass.empty()) continue; + + if (send(sock, pass.c_str(), pass.length(), 0) < 0) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to send" : "送信に失敗"); + close(sock); + return false; + } + + char res[256] = {0}; + int reslen = recv(sock, res, sizeof(res) - 1, 0); + if (reslen < 0) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to retrieve" : "受取に失敗"); + close(sock); + return false; + } + + res[reslen] = '\0'; + std::string response(res); + + if (response.compare(0, 1, "0") != 0) { + vulnpaths.push_back(dispath); + vulncount++; + } + + close(sock); + + // 再接続 + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == -1) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to create socket" : "ソケットを作成に失敗"); + return false; + } + + if (connect(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) { + fl_alert(lang.compare(0, 2, "en") == 0 ? + "Failed to reconnect" : "再接続に失敗"); + close(sock); + return false; + } + } + + close(sock); + + return true; +} + +void Vulnpass::showRes() { + std::string lang = Common::getlang(); + + std::string res; + if (lang.compare(0, 2, "en") == 0) { + res = "Breached passwords:\n"; + } else { + res = "漏洩したパスワード:\n"; + } + + for (const auto &path : vulnpaths) { + res += path + "\n"; + } + + if (lang.compare(0, 2, "en") == 0) { + res += "Total: " + std::to_string(vulnpaths.size()) + "\n"; + res += "It's advised to change any of the"; + res += "breached passwords as soon as possible!"; + } else { + res += "合計: " + std::to_string(vulnpaths.size()) + "\n"; + res += "漏洩したパスワードは出来るだけ早く変更する事をお勧めします!"; + } + + Fl_Window *win = new Fl_Window(500, 440, lang.compare(0, 2, "en") == 0 ? + "Results" : "結果"); + Fl_Text_Display *display = new Fl_Text_Display(10, 10, 480, 380); + Fl_Text_Buffer *textbuf = new Fl_Text_Buffer(); + + textbuf->text(res.c_str()); + display->buffer(textbuf); + display->scrollbar_width(15); + win->resizable(display); + + Fl_Button *okBtn = new Fl_Button(210, 400, 80, 30, "OK"); + okBtn->callback([](Fl_Widget *widget, void *win) { + (void)widget; + reinterpret_cast(win)->hide(); + }, win); + + win->add(okBtn); + + win->end(); + win->show(); +} + +void Vulnpass::dialog_cb(Fl_Widget *w, void *) { + (void)w; + + std::string lang = Common::getlang(); + + Fl_Window *dialog = new Fl_Window(400, 50, + (lang.compare(0, 2, "en") == 0 ? + "Check for branched password" : "漏洩されたパスワードの確認")); + + Fl_Box *box = new Fl_Box(10, 10, 380, 20, + (lang.compare(0, 2, "en") == 0 ? + "Checking, please wait for a while..." : + "確認中。暫くお待ち下さい・・・")); + + dialog->add(box); + + dialog->end(); + dialog->set_modal(); + dialog->show(); + + std::thread checker([dialog]() { + vuln.exec(); + + Fl::lock(); + dialog->hide(); + vuln.showRes(); + Fl::unlock(); + }); + + checker.detach(); +} diff --git a/src/vulnpass.hh b/src/vulnpass.hh new file mode 100644 index 0000000..8f06ff1 --- /dev/null +++ b/src/vulnpass.hh @@ -0,0 +1,18 @@ +#ifndef VULNPASS_HH +#define VULNPASS_HH + +#include + +#include "dialog.hh" + +class Vulnpass : public Dialog { + public: + Fl_Button *btn = nullptr; + + static void dialog_cb(Fl_Widget *w, void *); + void vuln_cb(Fl_Widget *, void *); + bool exec(); + void showRes(); +}; + +#endif