What changed, and why it matters
This change alters the Monero GUI wallet setup wizard so that newly created or recovered wallets are kept only in memory, rather than being written to a temporary file on disk during the setup process. The old approach created temporary wallet files on the computer's storage and then deleted them after the user finished the wizard. The patch removes the code that created and cleaned up those temporary files. This is a defensive improvement: it reduces the chance that an unfinished or temporary wallet file containing sensitive data is left behind on disk, exposed to other users, recovered by forensic tools, or leaked through backups, swap files, or file indexing. The change also adds a small UI guard so the wizard cannot be interacted with while a hardware-device wallet is being created asynchronously.
Treat this as a security-hardening improvement rather than an active vulnerability. Reviewers should verify that the underlying Monero wallet library correctly handles empty path strings for in-memory wallets across all supported platforms, that no temporary files are still created by lower-level code, and that the new `closeWizardWallet()` cleanup path is invoked reliably when the wizard is cancelled or restarted. Users should upgrade to a build containing this commit to reduce disk-side exposure of wallet secrets during creation/recovery.
Security signals we found
Eliminates temporary wallet files that could persist or leak sensitive key material before final wallet storage
Reduces attack surface from temporary file handling, file-permission issues, and incomplete cleanup
Adds UI state guard (`deviceWalletCreationInProgress`) to prevent user interaction during asynchronous hardware wallet creation
Destructor logic updated to avoid storing or logging paths for in-memory wallets
No explicit security advisory, CVE, or researcher attribution present in commit or supplied references
Evidence from the diff
The commit refactors wallet creation in the Monero GUI wizard to use in-memory wallets instead of temporary on-disk files. Key changes: (1) Wallet.cpp destructor now skips storing the wallet cache when m_walletImpl->path() is empty, so unsaved in-memory wallets are discarded cleanly. (2) oshelper.cpp/h removes temporaryFilename() and removeTemporaryWallet(), eliminating the helper functions that produced and cleaned up temp wallet files. (3) WizardController.qml replaces tmpWalletFilename with deviceWalletCreationInProgress, calls walletManager.createWallet/recoveryWallet/createWalletFromKeys/createWalletFromDeviceAsync with an empty path string instead of a temporary filename, and centralizes wallet cleanup in closeWizardWallet(). (4) PasswordDialog.qml respects deviceWalletCreationInProgress when re-enabling the wizard. The diff is a reduction of 37 lines and removes the temporary-file surface area entirely.
Changed components
Monero GUI wallet setup wizardsrc/libwalletqt/Wallet.cppsrc/main/oshelper.cpp / oshelper.hwizard/WizardController.qmlcomponents/PasswordDialog.qmlInspect captured patch +32 / −69
diff --git a/components/PasswordDialog.qml b/components/PasswordDialog.qml
index f369593..bd26694 100644
--- a/components/PasswordDialog.qml
+++ b/components/PasswordDialog.qml
@@ -110,7 +110,7 @@ FocusScope {
function close() {
leftPanel.enabled = true
middlePanel.enabled = true
- wizard.enabled = true
+ wizard.enabled = !wizard.deviceWalletCreationInProgress
if (rootItem.state == "wizard") {
titleBar.state = "essentials"
} else {
diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp
index 8e32b52..a6bf9c1 100644
--- a/src/libwalletqt/Wallet.cpp
+++ b/src/libwalletqt/Wallet.cpp
@@ -1220,7 +1220,9 @@ Wallet::~Wallet()
m_scheduler.shutdownWaitForFinished();
//Monero::WalletManagerFactory::getWalletManager()->closeWallet(m_walletImpl);
- if(status() == Status_Critical)
+ if (m_walletImpl->path().empty())
+ qDebug("Discarding unsaved in-memory wallet");
+ else if(status() == Status_Critical)
qDebug("Not storing wallet cache");
else if( m_walletImpl->store(""))
qDebug("Wallet cache stored successfully");
diff --git a/src/main/oshelper.cpp b/src/main/oshelper.cpp
index 5d189ac..5d517b9 100644
--- a/src/main/oshelper.cpp
+++ b/src/main/oshelper.cpp
@@ -35,7 +35,6 @@
#include <QFileDialog>
#include <QScreen>
#include <QStandardPaths>
-#include <QTemporaryFile>
#include <QWindow>
#include <QDir>
#include <QDebug>
@@ -193,27 +192,6 @@ QString OSHelper::openSaveFileDialog(const QString &title, const QString &folder
return QFileDialog::getSaveFileName(nullptr, title, hint);
}
-QString OSHelper::temporaryFilename() const
-{
- QString tempFileName;
- {
- QTemporaryFile f;
- f.open();
- tempFileName = f.fileName();
- }
- return tempFileName;
-}
-
-bool OSHelper::removeTemporaryWallet(const QString &fileName) const
-{
- // Temporary files should be deleted automatically by default, in case they wouldn't, we delete them manually as well
- bool cache_deleted = QFile::remove(fileName);
- bool address_deleted = QFile::remove(fileName + ".address.txt");
- bool keys_deleted = QFile::remove(fileName +".keys");
-
- return cache_deleted && address_deleted && keys_deleted;
-}
-
bool OSHelper::isCapsLock() const
{
#if defined(Q_OS_WIN)
diff --git a/src/main/oshelper.h b/src/main/oshelper.h
index b6e98d3..0f20a44 100644
--- a/src/main/oshelper.h
+++ b/src/main/oshelper.h
@@ -49,11 +49,9 @@ public:
Q_INVOKABLE bool openFile(const QString &filePath) const;
Q_INVOKABLE bool openContainingFolder(const QString &filePath) const;
Q_INVOKABLE QString openSaveFileDialog(const QString &title, const QString &folder, const QString &filename) const;
- Q_INVOKABLE QString temporaryFilename() const;
Q_INVOKABLE QString temporaryPath() const;
Q_INVOKABLE bool isWritableDirectory(const QString &path) const;
Q_INVOKABLE QString randomPassword(int numBytes = 32) const;
- Q_INVOKABLE bool removeTemporaryWallet(const QString &walletName) const;
Q_INVOKABLE bool isCapsLock() const;
Q_INVOKABLE quint8 getNetworkTypeFromFile(const QString &keysPath) const;
Q_INVOKABLE void openSeedTemplate() const;
diff --git a/wizard/WizardController.qml b/wizard/WizardController.qml
index a7932fd..6e722d8 100644
--- a/wizard/WizardController.qml
+++ b/wizard/WizardController.qml
@@ -52,7 +52,7 @@ Rectangle {
function restart(generatingNewSeed) {
// Clear up any state, including `m_wallet`, which
- // is the temp. wallet object whilst creating new wallets.
+ // is the in-memory wallet object whilst creating new wallets.
// This function is called automatically by navigating to `wizardHome`.
if(!generatingNewSeed) {
wizardController.walletOptionsName = defaultAccountName;
@@ -71,11 +71,14 @@ Rectangle {
wizardController.walletOptionsIsRecoveringFromDevice = false;
wizardController.walletOptionsDeviceName = '';
wizardController.walletOptionsDeviceIsRestore = false;
- wizardController.tmpWalletFilename = '';
wizardController.walletOptionsSubaddressLookahead = '';
disconnect();
- if (typeof wizardController.m_wallet !== 'undefined'){
+ closeWizardWallet();
+ }
+
+ function closeWizardWallet() {
+ if (typeof wizardController.m_wallet !== 'undefined') {
walletManager.closeWallet();
wizardController.m_wallet = undefined;
}
@@ -107,7 +110,7 @@ Rectangle {
property string walletOptionsSubaddressLookahead: ''
property string walletOptionsDeviceName: ''
property bool walletOptionsDeviceIsRestore: false
- property string tmpWalletFilename: ''
+ property bool deviceWalletCreationInProgress: false
// recovery made (restore wallet)
property string walletRestoreMode: 'seed' // seed, keys, qr
@@ -333,26 +336,19 @@ Rectangle {
}
function createWallet() {
- // Creates wallet in a temp. location
-
- // Always delete the wallet object before creating new - we could be stepping back from recovering wallet
- if (typeof wizardController.m_wallet !== 'undefined') {
- walletManager.closeWallet()
- console.log("deleting wallet")
- }
+ // Always close the in-memory wallet before creating a new one. We could
+ // be stepping back from recovering a wallet or generating a new seed.
+ closeWizardWallet();
- var tmp_wallet_filename = oshelper.temporaryFilename();
- console.log("Creating temporary wallet", tmp_wallet_filename)
+ console.log("Creating in-memory wallet")
var nettype = appWindow.persistentSettings.nettype;
var kdfRounds = appWindow.persistentSettings.kdfRounds;
- var wallet = walletManager.createWallet(tmp_wallet_filename, oshelper.randomPassword(), persistentSettings.language_wallet, nettype, kdfRounds)
+ var wallet = walletManager.createWallet('', oshelper.randomPassword(), persistentSettings.language_wallet, nettype, kdfRounds)
wizardController.walletOptionsSeed = wallet.seed
- // saving wallet in "global" object
- // @TODO: wallet should have a property pointing to the file where it stored or loaded from
+ // Keep the wallet in memory until the user chooses its final path.
wizardController.m_wallet = wallet;
- wizardController.tmpWalletFilename = tmp_wallet_filename
}
function writeWallet(onSuccess) {
@@ -374,10 +370,6 @@ Rectangle {
wizardStateView.wizardCreateWallet2View.seedListGrid.destroy();
}
- // make sure temporary wallet files are deleted
- console.log("Removing temporary wallet: " + wizardController.tmpWalletFilename)
- oshelper.removeTemporaryWallet(wizardController.tmpWalletFilename)
-
// save to persistent settings
persistentSettings.account_name = wizardController.walletOptionsName
persistentSettings.wallet_path = wizardController.m_wallet.path;
@@ -404,20 +396,17 @@ Rectangle {
var nettype = persistentSettings.nettype;
var kdfRounds = persistentSettings.kdfRounds;
var restoreHeight = wizardController.walletOptionsRestoreHeight;
- var tmp_wallet_filename = oshelper.temporaryFilename()
- console.log("Creating temporary wallet", tmp_wallet_filename)
- // delete the temporary wallet object before creating new
- if (typeof wizardController.m_wallet !== 'undefined') {
- walletManager.closeWallet()
- console.log("deleting temporary wallet")
- }
+ // Close any previous in-memory wallet before restoring a new one.
+ closeWizardWallet();
+
+ console.log("Creating in-memory recovery wallet")
var wallet = ''
// From seed or keys
if(wizardController.walletRestoreMode === 'seed')
- wallet = walletManager.recoveryWallet(tmp_wallet_filename, wizardController.walletOptionsSeed, wizardController.walletOptionsSeedOffset, nettype, restoreHeight, kdfRounds);
+ wallet = walletManager.recoveryWallet('', wizardController.walletOptionsSeed, wizardController.walletOptionsSeedOffset, nettype, restoreHeight, kdfRounds);
else
- wallet = walletManager.createWalletFromKeys(tmp_wallet_filename, persistentSettings.language_wallet, nettype,
+ wallet = walletManager.createWalletFromKeys('', persistentSettings.language_wallet, nettype,
wizardController.walletOptionsRecoverAddress, wizardController.walletOptionsRecoverViewkey,
wizardController.walletOptionsRecoverSpendkey, restoreHeight, kdfRounds)
@@ -425,7 +414,6 @@ Rectangle {
if (success) {
wizardController.m_wallet = wallet;
wizardController.walletOptionsIsRecovering = true;
- wizardController.tmpWalletFilename = tmp_wallet_filename
} else {
console.log(wallet.errorString)
appWindow.showStatusMessage(qsTr(wallet.errorString), 5);
@@ -459,17 +447,11 @@ Rectangle {
}
function createWalletFromDevice() {
- // TODO: create wallet in temporary filename and a) move it to the path specified by user after the final
- // page submitted or b) delete it when program closed before reaching final page
-
- // Always delete the wallet object before creating new - we could be stepping back from recovering wallet
- if (typeof wizardController.m_wallet !== 'undefined') {
- walletManager.closeWallet()
- console.log("deleting wallet")
- }
+ // Always close the in-memory wallet before creating a new one. We could
+ // be stepping back from recovering a wallet.
+ closeWizardWallet();
- tmpWalletFilename = oshelper.temporaryFilename();
- console.log("Creating temporary wallet", tmpWalletFilename)
+ console.log("Creating in-memory wallet from device")
var nettype = persistentSettings.nettype;
var kdfRounds = persistentSettings.kdfRounds;
var restoreHeight = wizardController.walletOptionsRestoreHeight;
@@ -477,11 +459,15 @@ Rectangle {
var deviceName = wizardController.walletOptionsDeviceName;
connect();
- walletManager.createWalletFromDeviceAsync(tmpWalletFilename, oshelper.randomPassword(), nettype, deviceName, restoreHeight, subaddressLookahead, kdfRounds);
+ wizardController.deviceWalletCreationInProgress = true;
+ wizardController.enabled = false;
+ walletManager.createWalletFromDeviceAsync('', oshelper.randomPassword(), nettype, deviceName, restoreHeight, subaddressLookahead, kdfRounds);
creatingWalletDeviceSplash();
}
function onWalletCreated(wallet) {
+ wizardController.deviceWalletCreationInProgress = false;
+ wizardController.enabled = true;
splash.close()
var success = wallet.status === Wallet.Status_Ok;
@@ -494,7 +480,6 @@ Rectangle {
}
} else {
console.log(wallet.errorString)
- wizardController.tmpWalletFilename = '';
appWindow.showStatusMessage(qsTr(wallet.errorString), 5);
walletManager.closeWallet();
}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.