Add keychain dialog and settings for unsecure backend usage

- Introduced KeychainDialog for managing keychain access during app store sign-in.
- Added settings for enabling/disabling unsecure backend usage in SettingsManager.
- Updated AppsWidget to initialize keychain dialog based on settings.
- Enhanced error messages and UI adjustments in various components.
This commit is contained in:
uncor3
2025-11-03 14:14:45 -08:00
parent c6867578b8
commit b1adde58dd
18 changed files with 352 additions and 51 deletions
+2 -1
View File
@@ -117,7 +117,8 @@ void AppDownloadBaseDialog::startDownloadProcess(const QString &bundleId,
// if (promptToOpenDir)
QMessageBox::critical(
this, "Download Failed",
QString("Failed to download %1. Error code: %2")
QString("Failed to download %1. Try signing out and back "
"in. Error code: %2")
.arg(m_appName)
.arg(result));
reject();
+19 -1
View File
@@ -1,5 +1,6 @@
#include "appstoremanager.h"
#include "libipatool-go.h"
#include "settingsmanager.h"
#include <QApplication>
#include <QDebug>
#include <QFuture>
@@ -46,7 +47,24 @@ AppStoreManager::AppStoreManager(QObject *parent)
bool AppStoreManager::initialize()
{
int result = IpaToolInitialize();
bool useUnsecureBackend =
SettingsManager::sharedInstance()->useUnsecureBackend();
QString backends;
if (useUnsecureBackend) {
backends = "file";
} else {
#ifdef __APPLE__
backends = "keychain,file";
#elif defined(WIN32)
backends = "wincred,file";
#else
backends = "secret-service,file";
#endif
}
int result = IpaToolInitialize(backends.toUtf8().data());
if (result != 0) {
qDebug() << "IpaToolInitialize failed with error code:" << result;
return false;
+49 -16
View File
@@ -5,7 +5,10 @@
#include "appinstalldialog.h"
#include "appstoremanager.h"
#include "iDescriptor-ui.h"
#include "keychaindialog.h"
#include "logindialog.h"
#include "mainwindow.h"
#include "settingsmanager.h"
#include "sponsorwidget.h"
#include "zlineedit.h"
#include <QApplication>
@@ -156,17 +159,6 @@ void AppsWidget::setupUI()
// --- Status and Login Button ---
m_manager = AppStoreManager::sharedInstance();
if (!m_manager) {
qDebug() << "AppStoreManager failed to initialize";
m_statusLabel->setText("Failed to initialize");
m_loginButton->setText("Failed to initialize");
m_loginButton->setEnabled(false);
m_loginButton->setStyleSheet(
"background-color: #ccc; color: #666; border: none; border-radius: "
"4px; padding: 8px 16px; font-size: 14px;");
} else {
onAppStoreInitialized(m_manager->getAccountInfo());
}
m_statusLabel->setStyleSheet("font-size: 14px; color: #666;");
@@ -213,8 +205,10 @@ void AppsWidget::setupUI()
&AppsWidget::onAppStoreInitialized);
connect(m_manager, &AppStoreManager::loggedOut, this,
&AppsWidget::onAppStoreInitialized);
}
// fetch sponsors
void AppsWidget::init()
{
QUrl sponsorsUrl("http://localhost:5173/sponsors.json");
QNetworkRequest request(sponsorsUrl);
QNetworkReply *reply = m_networkManager->get(request);
@@ -247,8 +241,8 @@ void AppsWidget::setupUI()
QJsonObject silverObj = sponsorObj["silver"].toObject();
QJsonObject bronzeObj = sponsorObj["bronze"].toObject();
// Store the platinum members to be used when populating the
// grid
// Store the platinum members to be used when populating
// the grid
m_platinumSponsors = platinumObj["members"].toArray();
m_goldSponsors = goldObj["members"].toArray();
m_silverSponsors = silverObj["members"].toArray();
@@ -259,16 +253,47 @@ void AppsWidget::setupUI()
}
}
qDebug() << "Sponsors fetch completed";
showDefaultApps();
reply->deleteLater();
QTimer::singleShot(0, this, &AppsWidget::handleInit);
} catch (...) {
qDebug() << "Exception occurred while processing sponsors";
showDefaultApps();
reply->deleteLater();
QTimer::singleShot(0, this, &AppsWidget::handleInit);
}
});
}
void AppsWidget::handleInit()
{
if (!m_manager) {
qDebug() << "AppStoreManager failed to initialize";
m_statusLabel->setText("Failed to initialize");
m_loginButton->setText("Failed to initialize");
m_loginButton->setEnabled(false);
m_loginButton->setStyleSheet(
"background-color: #ccc; color: #666; "
"border: "
"none; border-radius: "
"4px; padding: 8px 16px; font-size: 14px;");
return;
}
if (!SettingsManager::sharedInstance()->useUnsecureBackend() &&
SettingsManager::sharedInstance()->showKeychainDialog()) {
#ifdef __APPLE__
KeychainDialog dialog(this);
if (dialog.exec() == QDialog::Rejected) {
// pass empty QJsonObject to skip signing in
onAppStoreInitialized(QJsonObject());
showDefaultApps();
return;
}
#endif
}
// todo also change in the ipatoolinitialze as the backend is alrady enabled
onAppStoreInitialized(m_manager->getAccountInfo());
showDefaultApps();
}
void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo)
{
if (accountInfo.contains("success") &&
@@ -277,10 +302,13 @@ void AppsWidget::onAppStoreInitialized(const QJsonObject &accountInfo)
QString email = accountInfo.value("email").toString();
m_statusLabel->setText("Signed in as " + email);
m_isLoggedIn = true;
m_searchEdit->setDisabled(false);
} else {
m_statusLabel->setText("Not signed in");
m_searchEdit->setDisabled(true);
}
} else {
m_searchEdit->setDisabled(true);
m_statusLabel->setText("Not signed in");
}
@@ -710,6 +738,11 @@ void AppsWidget::createAppCard(
void AppsWidget::onDownloadIpaClicked(const QString &name,
const QString &bundleId)
{
if (!m_isLoggedIn) {
QMessageBox::information(this, "Sign In Required",
"Please sign in to download IPA files.");
return;
}
QString description = "Download the IPA file for " + name;
AppDownloadDialog dialog(name, bundleId, description, this);
dialog.exec();
+2 -1
View File
@@ -66,6 +66,7 @@ public:
static AppsWidget *sharedInstance();
void onAppCardClicked(const QString &appName, const QString &bundleId,
const QString &description);
void init();
private slots:
void onLoginClicked();
void onDownloadIpaClicked(const QString &name, const QString &bundleId);
@@ -90,7 +91,7 @@ private:
void clearAppGrid();
void populateDefaultApps();
void createSponsorCard(QGridLayout *gridLayout, int row, int col);
void handleInit();
QStackedWidget *m_stackedWidget;
QWidget *m_defaultAppsPage;
QWidget *m_loadingPage;
+21 -2
View File
@@ -245,11 +245,30 @@ void DNSSD_API DnssdService::addrInfoCallback(
inet_ntop(AF_INET, &addr_in->sin_addr, ip, sizeof(ip));
NetworkDevice device;
device.name = pending.name;
// Extract a better device name from hostname or use TXT records
QString friendlyName = pending.hostname;
if (friendlyName.endsWith(".local.")) {
friendlyName =
friendlyName.left(friendlyName.length() - 7); // Remove ".local."
qDebug() << "friendly name:" << friendlyName;
}
// Try to get device name from TXT records first
if (pending.txt.contains("DvNm")) {
device.name = pending.txt["DvNm"];
qDebug() << "Device name from DvNm TXT record:" << device.name;
} else if (pending.txt.contains("Name")) {
device.name = pending.txt["Name"];
qDebug() << "Device name from Name TXT record:" << device.name;
} else {
// Use the cleaned hostname as fallback
qDebug() << "Using hostname as device name:" << friendlyName;
device.name = friendlyName;
}
device.hostname = pending.hostname;
device.address = QString::fromUtf8(ip);
device.port = pending.port > 0 ? pending.port : 22; // Default to SSH port
// device.txt = pending.txt;
qDebug() << "Resolved IP for Apple device:" << device.name << "at"
<< device.address << ":" << device.port;
+135
View File
@@ -0,0 +1,135 @@
#include "keychaindialog.h"
#include "settingsmanager.h"
#include <QApplication>
#include <QCheckBox>
#include <QLabel>
#include <QMediaPlayer>
#include <QPushButton>
#include <QVBoxLayout>
#include <QVideoWidget>
KeychainDialog::KeychainDialog(QWidget *parent)
: QDialog(parent), m_player(nullptr), m_videoWidget(nullptr),
m_mainLayout(nullptr), m_okButton(nullptr), m_titleLabel(nullptr),
m_descriptionLabel(nullptr), m_dontShowAgainCheckbox(nullptr)
{
setupUI();
setupVideo();
}
KeychainDialog::~KeychainDialog()
{
if (m_player) {
m_player->stop();
}
}
void KeychainDialog::setupUI()
{
setWindowTitle("Keychain Access Required");
setModal(true);
setMinimumSize(600, 450);
resize(700, 500);
m_mainLayout = new QVBoxLayout(this);
m_mainLayout->setContentsMargins(20, 20, 20, 20);
m_mainLayout->setSpacing(15);
// Title label
m_titleLabel = new QLabel("Keychain Access Required");
m_titleLabel->setAlignment(Qt::AlignCenter);
m_titleLabel->setStyleSheet(
"font-size: 18px; font-weight: bold; margin-bottom: 10px;");
m_mainLayout->addWidget(m_titleLabel);
// Description label
m_descriptionLabel = new QLabel(
"In order to sign in to App Store we use the keychain backend to "
"safely store and retrieve your credentials. Please click on \"Always "
"Allow\" when prompted. "
"This is a security feature to protect your Apple ID credentials. You "
"can disable this in Settings.");
m_descriptionLabel->setAlignment(Qt::AlignCenter);
m_descriptionLabel->setWordWrap(true);
m_mainLayout->addWidget(m_descriptionLabel);
// Video widget
m_videoWidget = new QVideoWidget();
m_videoWidget->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
m_videoWidget->setAspectRatioMode(
Qt::AspectRatioMode::KeepAspectRatioByExpanding);
m_videoWidget->setStyleSheet(
"QVideoWidget { background-color: transparent; }");
m_videoWidget->setMinimumHeight(250);
m_mainLayout->addWidget(m_videoWidget, 1);
m_dontShowAgainCheckbox = new QCheckBox("Don't show this again");
m_mainLayout->addWidget(m_dontShowAgainCheckbox, 0, Qt::AlignCenter);
QHBoxLayout *buttonsLayout = new QHBoxLayout();
m_skipSigningInButton = new QPushButton("Skip For Now");
m_skipSigningInButton->setFixedHeight(40);
m_okButton = new QPushButton("OK, I understand");
m_okButton->setDefault(true);
m_okButton->setFixedHeight(40);
buttonsLayout->addWidget(m_skipSigningInButton);
buttonsLayout->addWidget(m_okButton);
m_mainLayout->addLayout(buttonsLayout, Qt::AlignCenter);
connect(m_okButton, &QPushButton::clicked, this,
&KeychainDialog::onOkClicked);
connect(m_skipSigningInButton, &QPushButton::clicked, this,
&KeychainDialog::onSkipSigningInClicked);
}
void KeychainDialog::setupVideo()
{
m_player = new QMediaPlayer(this);
m_player->setVideoOutput(m_videoWidget);
m_player->setSource(QUrl("qrc:/resources/keychain.mp4"));
// Loop the video
connect(m_player, &QMediaPlayer::mediaStatusChanged, this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::EndOfMedia) {
m_player->setPosition(0);
m_player->play();
}
});
// Auto-play when ready
connect(m_player, &QMediaPlayer::mediaStatusChanged, this,
[this](QMediaPlayer::MediaStatus status) {
if (status == QMediaPlayer::LoadedMedia) {
m_player->play();
}
});
}
void KeychainDialog::onOkClicked()
{
if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) {
SettingsManager::sharedInstance()->setShowKeychainDialog(false);
}
if (m_player) {
m_player->stop();
}
accept();
}
void KeychainDialog::onSkipSigningInClicked()
{
if (m_dontShowAgainCheckbox && m_dontShowAgainCheckbox->isChecked()) {
SettingsManager::sharedInstance()->setShowKeychainDialog(false);
}
if (m_player) {
m_player->stop();
}
reject();
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef KEYCHAIN_DIALOG_H
#define KEYCHAIN_DIALOG_H
#include <QCheckBox>
#include <QDialog>
#include <QLabel>
#include <QMediaPlayer>
#include <QPushButton>
#include <QVBoxLayout>
#include <QVideoWidget>
class KeychainDialog : public QDialog
{
Q_OBJECT
public:
explicit KeychainDialog(QWidget *parent = nullptr);
~KeychainDialog();
private slots:
void onOkClicked();
void onSkipSigningInClicked();
private:
void setupUI();
void setupVideo();
QMediaPlayer *m_player;
QVideoWidget *m_videoWidget;
QVBoxLayout *m_mainLayout;
QPushButton *m_okButton;
QPushButton *m_skipSigningInButton;
QLabel *m_titleLabel;
QLabel *m_descriptionLabel;
QCheckBox *m_dontShowAgainCheckbox;
};
#endif // KEYCHAIN_DIALOG_H
+6 -9
View File
@@ -134,21 +134,18 @@ MainWindow::MainWindow(QWidget *parent)
this, &MainWindow::updateNoDevicesConnected);
m_ZTabWidget->addTab(m_mainStackedWidget, "iDevice");
m_ZTabWidget->addTab(AppsWidget::sharedInstance(), "Apps");
auto *appsWidgetTab =
m_ZTabWidget->addTab(AppsWidget::sharedInstance(), "Apps");
m_ZTabWidget->addTab(new ToolboxWidget(this), "Toolbox");
auto *jailbrokenWidget = new JailbrokenWidget(this);
m_ZTabWidget->addTab(jailbrokenWidget, "Jailbroken");
m_ZTabWidget->finalizeStyles();
// connect(
// m_ZTabWidget, &ZTabWidget::currentChanged, this,
// [this, jailbrokenWidget](int index) {
// if (index == 3) { // Jailbroken tab
// jailbrokenWidget->initWidget();
// }
// },
// Qt::SingleShotConnection);
connect(
appsWidgetTab, &ZTab::clicked, this,
[this](int index) { AppsWidget::sharedInstance()->init(); },
Qt::SingleShotConnection);
// settings button
ZIconWidget *settingsButton = new ZIconWidget(
+24
View File
@@ -106,6 +106,17 @@ void SettingsManager::setUnmountiFuseOnExit(bool enabled)
}
#endif
bool SettingsManager::useUnsecureBackend() const
{
return m_settings->value("useUnsecureBackend-ipatool", false).toBool();
}
void SettingsManager::setUseUnsecureBackend(bool enabled)
{
m_settings->setValue("useUnsecureBackend-ipatool", enabled);
m_settings->sync();
}
QString SettingsManager::theme() const
{
return m_settings->value("theme", "System Default").toString();
@@ -128,6 +139,17 @@ void SettingsManager::setConnectionTimeout(int seconds)
m_settings->sync();
}
bool SettingsManager::showKeychainDialog() const
{
return m_settings->value("showKeychainDialog", true).toBool();
}
void SettingsManager::setShowKeychainDialog(bool show)
{
m_settings->setValue("showKeychainDialog", show);
m_settings->sync();
}
void SettingsManager::doIfEnabled(Setting setting, std::function<void()> action)
{
bool shouldExecute = false;
@@ -167,8 +189,10 @@ void SettingsManager::resetToDefaults()
#ifndef __APPLE__
setUnmountiFuseOnExit(false);
#endif
setUseUnsecureBackend(false);
setTheme("System Default");
setConnectionTimeout(30);
setShowKeychainDialog(true);
}
void SettingsManager::saveFavoritePlace(const QString &path,
+5
View File
@@ -55,6 +55,8 @@ public:
bool unmountiFuseOnExit() const;
void setUnmountiFuseOnExit(bool enabled);
#endif
bool useUnsecureBackend() const;
void setUseUnsecureBackend(bool enabled);
QString theme() const;
void setTheme(const QString &theme);
@@ -62,6 +64,9 @@ public:
int connectionTimeout() const;
void setConnectionTimeout(int seconds);
bool showKeychainDialog() const;
void setShowKeychainDialog(bool show);
// Utility method for conditional execution
void doIfEnabled(Setting setting, std::function<void()> action);
+42 -2
View File
@@ -105,6 +105,18 @@ void SettingsWidget::setupUI()
scrollLayout->addWidget(deviceGroup);
// === SECURITY SETTINGS ===
auto *securityGroup = new QGroupBox("Security");
auto *securityLayout = new QVBoxLayout(securityGroup);
m_useUnsecureBackend =
new QCheckBox("Use unsecure backend for app store (ipatool)");
m_useUnsecureBackend->setToolTip(
"Enabling this may put your Apple account at risk but you don't have "
"to deal with Apple keychain.");
securityLayout->addWidget(m_useUnsecureBackend);
scrollLayout->addWidget(securityGroup);
// Add stretch to push everything to the top
scrollLayout->addStretch();
@@ -158,7 +170,7 @@ void SettingsWidget::loadSettings()
}
m_connectionTimeout->setValue(sm->connectionTimeout());
m_useUnsecureBackend->setChecked(sm->useUnsecureBackend());
// Disable apply button initially
m_applyButton->setEnabled(false);
}
@@ -180,6 +192,27 @@ void SettingsWidget::connectSignals()
this, &SettingsWidget::onSettingChanged);
connect(m_connectionTimeout, QOverload<int>::of(&QSpinBox::valueChanged),
this, &SettingsWidget::onSettingChanged);
connect(m_useUnsecureBackend, &QCheckBox::toggled, this, [this]() {
// since this is unsafe if its being enabled, show a warning
if (m_useUnsecureBackend->isChecked()) {
auto reply = QMessageBox::warning(
this, "Warning",
"Enabling this will not encrypt your Apple account which is a "
"security risk. Are you sure you want to enable this?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply == QMessageBox::Yes) {
m_restartRequired = true;
onSettingChanged();
} else {
m_useUnsecureBackend->setChecked(false);
}
} else {
m_restartRequired = true;
onSettingChanged();
}
});
}
void SettingsWidget::onBrowseButtonClicked()
@@ -223,7 +256,13 @@ void SettingsWidget::onResetToDefaultsClicked()
void SettingsWidget::onApplyClicked()
{
saveSettings();
QMessageBox::information(this, "Settings", "Settings have been applied.");
QMessageBox::information(this, "Settings",
m_restartRequired
? "Settings applied. Please restart "
"the application for changes to "
"take effect."
: "Settings applied.");
m_restartRequired = false;
}
void SettingsWidget::onSettingChanged()
@@ -244,6 +283,7 @@ void SettingsWidget::saveSettings()
#ifndef __APPLE__
sm->setUnmountiFuseOnExit(m_unmount_iFuseDrives->isChecked());
#endif
sm->setUseUnsecureBackend(m_useUnsecureBackend->isChecked());
sm->setTheme(m_themeCombo->currentText());
sm->setConnectionTimeout(m_connectionTimeout->value());
+3 -1
View File
@@ -40,7 +40,7 @@ private:
#ifndef __APPLE__
QCheckBox *m_unmount_iFuseDrives;
#endif
QCheckBox *m_useUnsecureBackend;
// Device Connection
QSpinBox *m_connectionTimeout;
@@ -48,6 +48,8 @@ private:
QPushButton *m_checkUpdatesButton;
QPushButton *m_resetButton;
QPushButton *m_applyButton;
bool m_restartRequired = false;
};
#endif // SETTINGSWIDGET_H
+1 -14
View File
@@ -29,23 +29,16 @@ void WelcomeWidget::setupUI()
m_mainLayout->addSpacing(12);
// Subtitle
m_subtitleLabel = createStyledLabel("100% Open-Source & Free", 16, false);
m_subtitleLabel = createStyledLabel("Open-Source & Free", 16, false);
m_subtitleLabel->setAlignment(Qt::AlignCenter);
QPalette palette = m_subtitleLabel->palette();
palette.setColor(QPalette::WindowText,
palette.color(QPalette::WindowText).lighter(140));
m_subtitleLabel->setPalette(palette);
m_mainLayout->addWidget(m_subtitleLabel);
m_mainLayout->addSpacing(10);
m_imageLabel = new ResponsiveQLabel();
m_imageLabel->setPixmap(QPixmap(":/resources/connect.png"));
// Let the pixmap scale while preserving aspect ratio
m_imageLabel->setScaledContents(true);
// Prefer centered, not full-width expansion
m_imageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// Cap size so it stays nicely centered on large windows
// m_imageLabel->setMaximumSize(480, 320);
m_imageLabel->setStyleSheet("background: transparent; border: none;");
@@ -53,15 +46,9 @@ void WelcomeWidget::setupUI()
m_mainLayout->addWidget(m_imageLabel, 0, Qt::AlignHCenter);
m_mainLayout->addSpacing(10);
// Instruction text
m_instructionLabel = createStyledLabel(
"Please connect an iOS device to get started", 14, false);
m_instructionLabel->setAlignment(Qt::AlignCenter);
QPalette instructionPalette = m_instructionLabel->palette();
instructionPalette.setColor(
QPalette::WindowText,
instructionPalette.color(QPalette::WindowText).lighter(120));
m_instructionLabel->setPalette(instructionPalette);
m_mainLayout->addWidget(m_instructionLabel);
m_mainLayout->addSpacing(10);
+2 -2
View File
@@ -72,7 +72,7 @@ void ZTabWidget::setupGlider()
m_gliderAnimation->setEasingCurve(QEasingCurve::OutCubic);
}
int ZTabWidget::addTab(QWidget *widget, const QString &label)
ZTab *ZTabWidget::addTab(QWidget *widget, const QString &label)
{
ZTab *tab = new ZTab(label, m_tabBar);
connect(tab, &ZTab::clicked, this, &ZTabWidget::onTabClicked);
@@ -84,7 +84,7 @@ int ZTabWidget::addTab(QWidget *widget, const QString &label)
m_stackedWidget->addWidget(widget);
m_buttonGroup->addButton(tab, index);
return index;
return tab;
}
void ZTabWidget::setCurrentIndex(int index)
+1 -1
View File
@@ -27,7 +27,7 @@ class ZTabWidget : public QWidget
public:
explicit ZTabWidget(QWidget *parent = nullptr);
void finalizeStyles();
int addTab(QWidget *widget, const QString &label);
ZTab *addTab(QWidget *widget, const QString &label);
void setCurrentIndex(int index);
int currentIndex() const;
QWidget *widget(int index) const;