This document is deliberately blunt. A wallet's job is to protect money, and the fastest way to lose money is to overestimate what a piece of software actually guarantees.
Encryption at rest. The vault file is sealed with AES-256-GCM. The 256-bit key is derived from your password with Argon2id (default: 256 MiB memory, 4 iterations, 4 lanes), which is memory-hard and resists GPU/ASIC brute-forcing far better than PBKDF2 or bcrypt. Every slot has its own random 16-byte salt and 12-byte nonce. GCM is authenticated, so any tampering with the file is detected on unlock rather than silently producing garbage.
No plaintext password handling in the security core. Passwords are converted to a pinned, zeroable buffer, run through the KDF, and the derived key is used directly. "Is this the right password?" is answered by whether the GCM authentication tag verifies, never by comparing stored password material.
What a password opens. Each password opens a profile: one or more wallets (from a seed, from keys, or watch-only), an address book, and the subaddress labels, account names and transaction notes of each wallet. All of it is sealed together in that password's slot. In a vault created by 0.5, or upgraded to its format, every slot is 256 KiB of encrypted data however much it holds, so the file is the same size for one wallet or twenty, with or without contacts. A vault from 0.2 or 0.3 keeps its 4 KiB slots until you upgrade it (Upgrading to 0.5). When a profile no longer fits, the app refuses the change and says so; nothing is written.
Duress / plausible deniability. The file always contains two equal-sized slots. Without a correct password an adversary cannot tell whether the second slot is a decoy profile or random filler, cannot tell which physical slot is real (position is randomised at write time), and cannot tell how many wallets exist. Unlock does constant work across all slots.
The duress scenario is the one where the adversary DOES hold a password (the duress one) plus the device. So the decoy must also be indistinguishable from the inside:
- Every slot decrypts to the same JSON shape (payload v3: a list of wallets and contacts, with no "real"/"decoy" kind anywhere). An examiner who decrypts the decoy with the duress password sees exactly what a single-password vault looks like. Vaults created by 0.1 are the exception until each slot has been converted; see Upgrading from 0.1.
- Every operation is symmetric: unlocking, adding, renaming or removing wallets, contacts, labels, notes, a wallet's node, and changing the password work identically for either profile, with identical wording. An open profile re-seals only its own slot, with the key derived at unlock (no Argon2 per change); the other slot is copied back byte for byte. Changing the password from inside an open wallet changes that profile's password only, whichever it is. A duress unlock does the same key-derivation work as a normal one.
- Wipe-on-duress fires on ANY use of the duress password, then clears its own flag (re-sealing with the key already derived, so it takes no extra time). Afterwards the vault is a single-wallet vault.
- The app's log records nothing that differs between the two wallets (no nodes, restore heights or send events).
The local wallet RPC. monero-wallet-rpc listens on 127.0.0.1 only while a wallet is open, and
requires per-session random HTTP Digest credentials. They are passed through a private config file
(0600, inside a 0700 session folder) rather than the command line (other local users can read a
process's command line), and the file is shredded once the server is up. This blocks web pages
(cross-site POSTs, DNS rebinding) and other local users from driving the open wallet. Digest auth
cannot authenticate the server (monero sends no rspauth), so before any seed is sent (and before
a new seed is generated) the app checks that every listener that could answer on that port belongs
to the process it started and that the process is still running (Linux, Windows). The check fails
closed: if ownership can't be confirmed (including on macOS, where the check is not yet
implemented) nothing is sent. wallet-rpc's own log and ring database
are kept in the same session folder, which is shredded on lock.
The wallet-rpc binary. It is only ever started from a fully-qualified path. A bare name in
Settings ("monero-wallet-rpc") is looked up on PATH; a relative path is refused. Handing a
relative name to the OS would make it search the app's folder and then the current directory,
so a file planted wherever the app was launched from could be started and handed the seed.
Memory forensics. .NET is a garbage-collected runtime. SecureBuffer pins and zeroes the
buffers it owns, but the moment a password exists as a string (e.g. the unlock field bound
in UnlockViewModel) the CLR may have already made immutable copies on the managed heap that
we cannot reliably wipe. The Argon2id library (Konscious) also keeps an internal copy of the
password bytes that is not zeroed after key derivation. A determined attacker with a memory
dump of the running, unlocked process can likely recover secrets. Locking the wallet and
closing the app is your defence; an unlocked wallet on a compromised machine is compromised.
A compromised operating system. Keyloggers, malicious kernels, screen capture, and hypervisor-level attackers defeat any user-space wallet. This app cannot protect a password typed into a machine that is already owned. Other programs in your desktop session can also read what the app shows (screen capture, or accessibility APIs for visible text such as your seed words while they are displayed). Masked password boxes are the exception: since 0.3 they accept input from assistive tools but never hand their text out.
Secure deletion on SSDs. The temp-file shredder overwrites bytes before deleting, but on SSDs with wear-levelling, and on copy-on-write or journaling filesystems, the original blocks may physically remain. Treat "shred" as best-effort, not a guarantee. For strong guarantees, use full-disk encryption underneath this app.
Several wallets on one remote node. Each open wallet runs its own monero-wallet-rpc and syncs on its own (the one on screen every few seconds, the others once a minute). A node you don't run sees all of them asking from the same IP address at the same times, and can reasonably guess they belong to one person. Use your own node, or route the connection over Tor (the built-in Tor, or your own SOCKS proxy, in Settings), if your wallets must not be linked. Wallets start syncing only once you open them after unlocking.
What you export or share. History CSV exports include your transaction notes, in plain text. A payment proof (transaction key) proves one payment and nothing else; a reserve proof tells whoever checks it how much you proved (at least the amount you gave, or everything the wallet holds when you gave none); a signed message proves you control the address. A watch-only wallet's private view key, like a seed, is sealed in the vault: anyone holding it sees every payment the wallet receives.
Deniability against a sophisticated adversary. The design defeats inspection of the vault file, including by someone holding the duress password. It does not defeat an adversary who can observe your daemon (a remote node sees your wallet's sync requests and broadcasts), correlate on-chain activity, image your RAM while unlocked, or find OS-level artifacts (recent-files lists, swap, crash dumps, file-system journals). Specifically:
- Copies of the vault taken at different times show which slots changed. If both slots ever
changed between two copies (e.g. you changed both passwords), both are live wallets. Hidden
volumes share this limitation. Since 0.5 more actions change a slot: adding or editing a wallet,
contact, label or note, handing out a subaddress, changing a wallet's node, even switching wallets
(the vault remembers the one you used last) re-seal that password's slot. Unlocking is no longer an exception:
every successful unlock now re-seals the opened slot (fresh nonce, no extra key derivation) and
rewrites the vault, so a duress unlock (which must write to wipe) and a normal unlock leave the
same trace on disk. Residuals of that rewrite: the write is best-effort and silent, so when it
fails (locked file, read-only media, full disk) the unlock still succeeds and writes nothing,
which differs from the other profile's unlock on the same machine; the file's mtime, a short-lived
vault.xv.tmp, backup/sync-client activity and file-system journals show that you unlocked, never which password; and the duress unlock additionally shredsvault.xv.replaced-*copies, and a copy of the vault taken before and after still shows which slot changed (the opened one), now on every unlock, not only on the duress one. - A wipe flag examined before it fires. If you enable wipe-on-duress, an examiner who decrypts the
decoy offline (without the app ever opening it) can read
wipeOther: trueand infer a second wallet existed. The instruction has to be readable with the duress password; once the app opens the decoy, the flag is consumed and gone. Without wipe-on-duress the decoy is indistinguishable. - macOS: the port-ownership check is not implemented there. The app fails closed: it refuses to send a seed (or generate one) to the wallet backend until a macOS ownership check exists. Releases still build an osx-arm64 binary, but seed-bearing use on macOS is non-functional until then.
- Vaults upgraded from 0.1: see Upgrading from 0.1.
- The ring database now lives only for the session. Monero keeps it so a wallet re-uses the same rings across a chain split; losing it each session only matters during a contentious fork.
True hidden-volume deniability à la VeraCrypt is a much harder problem than this addresses. If your threat model includes a state-level adversary with physical access, do not rely on this alone.
The "wipe on duress" option is irreversible. If you enable it, any use of the duress password (unlocking, or changing its password) permanently destroys the real slot on that device, along with vault copies the app kept beside it (Settings → Restore). Exported backups stored elsewhere are out of its reach. If your seed is not backed up elsewhere, your funds are gone. This is a feature, and it is a foot-gun: never use the duress password yourself.
Weak passwords. Argon2id raises the cost per guess, but a short or common password is
still guessable offline by anyone with a copy of the vault. Use a long, high-entropy passphrase.
The app refuses the weakest passwords (common words, sequences, keyboard runs, repeats), but its
estimator is a heuristic, not a full dictionary model (zxcvbn would be better).
What else is on disk (and unencrypted). Besides the encrypted vault, XaultWallet writes
two plaintext files under %APPDATA%\XaultWallet\ (~/.config/XaultWallet/ on Linux,
~/Library/Application Support/XaultWallet/ on macOS):
settings.json (your monero-wallet-rpc and tor paths, default node and network, refresh and
auto-lock intervals, the Tor or proxy choice, whether amounts are hidden, window size) and logs/
(high-level events and error types). These deliberately contain no secrets (no seeds,
passwords, keys, or RPC credentials) and nothing that distinguishes one wallet from the other, so
they are not encrypted. While a wallet is open, a private xaultwallet_* folder in your temp
directory holds wallet-rpc's restored wallet, its log and ring database; it is shredded on lock and
swept on the next launch after a crash. If you download a seed backup, that file is plaintext by
design; store it offline and delete any on-disk copy.
File permissions (Linux/macOS). The data folder is 0700 and everything the app writes in it
(vault, settings, logs, kept vault copies) is 0600, whatever your umask. Installs made by earlier
versions are tightened at startup. Vault exports, seed backups and history exports are also written
0600 when the save dialog hands back a local path. (Before this, a umask of 022, the common
default, left the vault and any seed backup readable by every account on the machine.) On Windows
the per-user profile ACLs on %APPDATA% provide the equivalent; the app changes nothing there.
0.2 changed what is sealed inside each slot (payload v2; 0.5's payload v3, described above, keeps the same one-shape-for-every-slot property). The app can only re-seal a slot while it is open, so a vault created by 0.1 is converted one slot at a time, each by its own password:
- The first unlock with a password converts that password's slot. The app then shows a one-time notice, the same notice for either password, so it says nothing about which wallet opened.
- Until the duress password is used once in 0.2, a decoy slot made by 0.1 still contains
"kind":1(and"duressWipeReal") inside its encryption. Anyone holding the vault file and the duress password can read that and know a second wallet exists: the exact leak 0.2 removes. - Using the duress password converts its slot and, if you enabled wipe-on-duress in 0.1, also wipes the main wallet, exactly as it always would have. Don't "unlock with it once to upgrade it" while that option is on.
- Copies made before the upgrade (exported backups, file-system snapshots, sync/cloud history) stay in the 0.1 format forever.
If you never set a duress password in 0.1, the upgrade is complete after your first unlock. If you did, the clean fix is to rebuild the vault:
- Have both seeds (main and decoy) and their restore heights written down, and check them.
- Settings → Data folder, quit the app, and move
vault.xvsomewhere offline as a fallback. - Start the app (it now offers to create a vault), choose Import for the main seed and for the decoy seed, and set both passwords.
- Unlock with each password to confirm the right wallet opens. Then destroy the old
vault.xvand every exported copy of it.
0.5 seals a whole profile in each password's part of the vault, which needs more room than 0.2 and 0.3 gave it. A vault they wrote (format 1: 4 KiB per password) opens in 0.5 and stays in format 1 until you upgrade it in Settings → Vault format (shown while one of its wallets is open). Its format and size never change on their own, though every unlock re-seals the part that password opens.
- In the old format each password's part holds about 4 KiB: a wallet or two with some labels, notes and contacts. A change that doesn't fit is refused, nothing is written, and the message points to the upgrade. 0.5 saves a password's part in its own layout the first time it writes it (the first unlock with that password, unless the part no longer fits in 4 KiB, in which case it is left as it was, and on any change). 0.3 can still open the file and any part no 0.5 unlock has re-sealed yet; it refuses a saved one as "created by a newer version of XaultWallet", and changes nothing.
- Upgrading turns the file into format 2 (256 KiB per password, as every vault 0.5 creates). The part your password opens is re-sealed in it. The other part is carried over byte for byte, at the start of its new, larger slot (the rest is random), and is re-sealed when its own password next opens it; until then it opens as before, because every slot is tried both ways on every unlock.
- There is no way back. 0.3 and earlier can't open a format 2 file: they report that the vault file has an unexpected size, and change nothing. If you might return to 0.3, export a backup (Settings → Export backup) first, and keep it until you're sure.
- A second password. Until it has been used in 0.5, its part shows that, to whoever can open
that part, which is to say whoever holds that password: in the old format its contents are still
in 0.3's layout, and after an upgrade it is a carried part. That is exactly the duress scenario: an
examiner with the duress password and the vault file could see that the decoy hasn't been used
since the vault was, and conclude that another password has been. So:
- Without wipe-on-duress: unlock with the duress password in 0.5 before anything else, and wait until its wallet is ready (its address shows). If you want the bigger format, upgrade from there. Then lock and use your main password as usual: its part is re-sealed when it opens.
- With wipe-on-duress: don't: any use of the duress password wipes the main wallets. Upgrade from your main password if you need the room, knowing an examiner who decrypts the decoy can tell (as they can already read its wipe flag, see above). For a vault without either sign, rebuild it in 0.5 from both seeds (the steps under Upgrading from 0.1).
- Vaults created by 0.1 open the same way, and everything in Upgrading from 0.1 still applies to them.
Bringing your own monero-wallet-rpc, downloaded and verified yourself, remains the recommendation: then you, not this app, decide what you trust. For everyone else, Download & install (on the startup screen and in Settings) does what a careful person does by hand, and installs nothing unless every step succeeds:
- It fetches
https://www.getmonero.org/downloads/hashes.txtand checks its OpenPGP signature against binaryFate's key, which ships inside the app (Monero's ownutils/gpg_keys/binaryfate.asc) and is accepted only for the pinned fingerprints81AC 591F E9C4 B65C 5806 AFC3 F0AF 4D46 2A0B DF92(primary key) andAD56 4CDA 8F16 65AC E78B 5DFD 2593 838E ABB1 F655(signing subkey). The verifier is deliberately narrow (v4 RSA, canonical-text signatures, SHA-256/512) and refuses anything else, including text outside the signed block. - It downloads this computer's official CLI archive from
downloads.getmonero.organd requires its SHA-256 to be the signed one. - It extracts only
monero-wallet-rpc, runs it once with--version(after the checks, never before), and installs it in your user profile (on Windows%LOCALAPPDATA%\XaultWallet\monero-cli, on Linux~/.local/share/XaultWallet/monero-cli), replacing an older one, but never a newer one already on disk, and never a release older than the app's built-in minimum (WalletRpcInstaller.MinimumVersion). The signature's creation time is not checked (the verifier does not parse it).
What this trusts: the copy of binaryFate's key in the XaultWallet release you are running (verify
that release's SHA256SUMS), and that key itself. A build signed with it is accepted; one that
isn't, isn't. getmonero.org sees your IP address unless the built-in Tor is on or you have set your own SOCKS
proxy (Settings); the download then goes through it.
Download & install Tor (Settings, Network & privacy, with "Tor, built in" chosen) works like the monero-wallet-rpc installer, and installs nothing unless every step succeeds:
- It asks Tor Project's update channel (
aus1.torproject.org) which Tor Browser release is current. That answer is not signed, so it is only used to pick a folder: it must be a plain version number (no letters, no slashes), not older than the app's minimum (TorInstaller.MinimumVersion, 15.0), and not older than a Tor already installed. - It fetches that release's
sha256sums-signed-build.txtand its detached signature fromdist.torproject.org, and checks the signature against the Tor Browser Developers key, which ships inside the app (exported unchanged from Tor Project's own WKD). Only the current signing subkey022D A248 432D 2A0E 0F54 E65E 316C 1FAC D62D 07D9is trusted, not the primary keyEF6E 286D DA85 EA2A 4BA7 DE68 4E2C 6E87 9329 8290(certification only) and not older subkeys. The unit tests verify a real published list and signature with this key. When Tor Project rotates to a new subkey (this one expires 2028-11-28), installs fail with "no valid signature" until an update of XaultWallet pins the new one: refusing is the safe failure. - It downloads this computer's Tor Expert Bundle and requires its SHA-256 to be the signed one. The file names in the signed list carry the version, so a list from another release can't vouch for this download.
- It unpacks only what tor needs (the binary, its libraries on Linux and macOS, and the GeoIP
files), never using a path from inside the archive to decide where to write; runs it once with
--version; and installs it in your user profile (on Windows%LOCALAPPDATA%\XaultWallet\tor, on Linux~/.local/share/XaultWallet/tor).
Tor can't download itself, so this download goes directly to torproject.org (or through your own SOCKS proxy if you have set one instead): Tor Project and anyone watching your connection can see that this computer downloaded Tor. You can also point Settings at a tor you installed yourself.
With "Tor, built in" chosen, XaultWallet runs its own tor as a child process: a client only (no
relay, no control port), listening as a SOCKS proxy on a random loopback port, with its own data
directory (%LOCALAPPDATA%\XaultWallet\tor-data on Windows, ~/.local/share/XaultWallet/tor-data on
Linux, kept between runs so it doesn't fetch the whole
consensus every start) and every system torrc ignored. It starts with the app, is tied to the app's
lifetime (a kill-on-close job object on Windows, and Tor's own __OwningControllerProcess
everywhere), and stops when the app exits.
What goes through it: each wallet backend's node traffic (monero-wallet-rpc --proxy), the app's
own node checks (sync height, node tests), and the monero-wallet-rpc download. It fails closed:
while Tor is chosen but not connected, wallets wait ("Connecting to Tor"), node checks are skipped,
and nothing falls back to a direct connection. A wallet started through Tor keeps that route until
it is opened again; if Tor stops, its traffic stops with it. The wallet screen says which route a
wallet uses (TOR, PROXY, DIRECT or LOCAL NODE). Changing a wallet's node (Manage) keeps that: a node
that is reached the other way (a remote node for a wallet opened on a node on this computer, which has
no proxy, or the reverse) restarts the wallet's backend for it, instead of switching it in live, where
monero-wallet-rpc would reach the remote node directly and the app's own node checks would too.
Exceptions, deliberately: a node on this computer (127.0.0.1, localhost) is reached directly,
because Tor refuses loopback destinations and that traffic never leaves the machine; and the Tor
download itself, as above.
What Tor does not hide: the node still sees which blocks the wallet asks for and the transactions it broadcasts (just not your IP address); several wallets opened through the same Tor can share circuits, so a node may see that the same Tor user syncs all of them; and Tor's presence (the install and its data folder) is visible on this computer.
The Coins tab lists the selected account's unspent outputs (each payment received is one), with the
address label it arrived at. Freezing a coin (monero-wallet-rpc's freeze) makes the wallet
leave it out of every transaction, Send max included, until it is unfrozen. Why it matters even with
Monero's ring signatures: a transaction that spends several of your coins together tells its
recipient, and anyone they share it with, that those coins belong to one person. Freezing a coin you
received from, say, an exchange keeps it out of an everyday payment.
The wallet file that holds wallet-rpc's own frozen flags is restored from the seed on every unlock
and shredded on lock, so the frozen list lives in the vault ("frozen", key images only). Every
slot carries that field, empty or not, so it doesn't distinguish a real profile from a decoy. After
an unlock, a coin is frozen again as soon as the scan finds it, and every send re-applies the list
right before the transaction is built, so a coin found since the last refresh can't slip in. A
watch-only wallet can't freeze (computing a key image needs the spend key).
The Chrome and Firefox extension (0.7, docs/BROWSER-COMPANION.md) can ask the running app for its state, put a payment request in the Send tab and get a new receive address. It never holds a key, a seed, a password or a balance, and it stores nothing, so an infostealer that copies the browser profile gets nothing of the wallet. Payments are confirmed in the app, never in the browser.
The companion is off until you connect a browser in Settings: before that the app answers no
browser request (not-connected). Its local channel is open whenever the app runs, because a second
launch hands a clicked monero: link (or "show me") to the running copy through it; such a hand-off can
only open a payment request for you to review or bring the window forward, never read the state or get
an address. The sender names itself ("chrome", "firefox" or "link") and a program running as you can
claim any of them, so this limits what an install that never used the companion exposes; it doesn't
authenticate anyone. The first time a connected browser's extension reaches the app, the app asks A
browser extension wants to connect (browser and extension id; Allow and Deny, Allow held a moment like
Send now) and remembers the answer until you disconnect that browser. That makes a first contact
visible, including an unpacked copy carrying the same key (it has the same id, so the question looks the
same: if you didn't just use the companion, deny it); it is not a secret a program running as you couldn't
get around by talking to the channel as "link" (which can only open a payment request) or by pressing Allow
for you while Enhanced Security is off.
It reaches the app through the browser's native messaging (XaultWallet's own executable, started by
the browser only for the extension the host manifest names) and a local channel only your user
account can open (a named pipe that also refuses connections from the network, or a Unix socket in a
private folder). The browser starts the host only for the extension ids in its manifest
(allowed_origins); the Chrome id comes from the public key in the extension's manifest, so it is not
a secret: an extension loaded unpacked with that key copied gets the same id and can talk to the app
(as can anything running as you, which can use the channel directly anyway). The check keeps other
installed extensions out, not a deliberate copy. Once the companion is in the Chrome Web Store, a
release will allow only the store's id. The native host passes back only the fields each kind of reply has, checked. Replies
carry the state (locked, unlocked, not set up, and whether an unlocked wallet is ready) and, when asked
for and allowed, one new subaddress with its network: never a wallet's name, balance, history, the
app's version, or the network or kind of the wallet on screen. At most 6 payment requests, 5 new addresses, 10 show and 60
status requests a minute are accepted.
Requests in the app. A request's own words (a description, a recipient name) are cleaned of anything that changes how text reads (line breaks, right-to-left overrides, invisible characters) and shown in quotes after where the request came from. A request never replaces a payment being confirmed, and one that would replace a form you filled, or a payment that may have gone out and needs checking, waits behind Open it instead. Review and Send now ignore clicks for a moment after a request lands or the confirmation appears. A new receive address is made only after you press Give address in the app for that site (a request just asks; like Send now, it ignores clicks for a moment after it appears, because the window comes forward with it; it takes a click, no key presses it; with Enhanced Security on, input from another program is dropped), so a program on the channel can't quietly add subaddresses to your vault; it is announced across the top of the window with its start and end, and only while the wallet is on screen. It is labelled just Browser unless you turn on Label browser addresses with the site (Settings): off, the vault doesn't record which sites you gave addresses to. While the app is locked, the lock screen says only that a request waits, not where it came from; a request waits at most 10 minutes.
Which profile is open. Status doesn't say the network of the wallet on screen or whether it is
watch-only (0.7.0a and earlier did). A new address still carries its network, so once you give the
browser an address, a decoy on another network than your real wallet can be told apart; keep them on
the same network if that matters to you.
The companion itself is visible, though: the installed extension, the native-host manifests (and,
on Windows, their registry keys) and a registered monero: handler show that XaultWallet is
installed and used with a browser. None of them says whether a vault holds a hidden wallet: they
reveal the app, not the decoy.
Tripwire. At every start, and when the window comes back to the front, the app checks that each browser connection it set up still leads to it, comparing the exact path it wrote, and that the program file there is still, byte for byte (SHA-256), the one that was running when the app started: a file replaced at the same path is reported too (after an update, restart the app). A connection that leads elsewhere says whether that program is an identical copy of XaultWallet. Release builds aren't Authenticode-signed, so there is no signature to compare; a file that can't be read is not reported (the check warns, it never blocks). A connection repointed to another program (to stand in for XaultWallet, for example to hand the browser its own "receive" address) brings up a warning naming that program; one that was removed is reported once, then forgotten. On Windows the app also notices when another program created its channel before XaultWallet started.
What it can't stop: a program running as you can rewrite those registrations, talk to the channel itself (proposing payments you must still confirm, or asking for subaddresses you must still allow), or simply read your files and keystrokes. The companion adds nothing such a program could steal, but it can make the browser show an address of its own: by repointing a registration (the tripwire warns about that), by answering on the channel in XaultWallet's place, or by editing an extension you loaded unpacked. XaultWallet announces every address it gives the browser with its first and last characters; check that, or the Receive tab, before you rely on an address the browser shows.
Settings, Enhanced Security (Windows, off by default). While it is on:
- Screen capture. The window is left out of screenshots, screen recordings, Windows Recall, and
screen sharing and remote-viewing tools that capture the screen (
SetWindowDisplayAffinitywithWDA_EXCLUDEFROMCAPTURE, the protection VeraCrypt uses; Windows before 10 2004 shows it black instead). When Enhanced Security is on as XaultWallet starts, menus and lists are drawn inside the window so they share it; after turning it on in Settings, restart XaultWallet for them to be covered. Windows' own file dialogs (where to save a backup, for example) are separate windows and are not covered. A camera pointed at the screen still sees it, and Microsoft documents this as content protection, not a security boundary. - Input. Only key presses and clicks Windows reports as coming from a device (or from the system,
or from an accessibility program Windows trusts with UIAccess, such as its on-screen keyboard) act in
XaultWallet (
GetCurrentInputMessageSource). Input injected withSendInput, and fake window messages posted or sent by another program (how hidden-VNC malware drives a window), are dropped and logged once. Windows documents how it labels sent messages but not posted ones; XaultWallet's tests check that injected ones are dropped. A program can still type characters into the field that has focus (it can't press Enter or click), and mouse moves are not checked, though only moves from a device keep the wallet from locking itself when you're away. - Accessibility. The window answers no UI Automation or MSAA requests, so other programs can neither read its text (addresses, balances, a seed being shown) nor press its buttons that way.
- Memory. XaultWallet and the monero-wallet-rpc it starts get an access list that stops other
programs of your account from opening them to read or write their memory or to start a thread in
them, and from changing that list back (an "OWNER RIGHTS" entry takes away the owner's implicit right
to). It closes the common way in, not every way: a program running as you can still get code into
XaultWallet without opening it (a window hook, or through one of its threads), a program that opened
XaultWallet before the list was applied keeps its access, and programs running as an administrator
or with the debug privilege are not stopped at all. (The published app also ignores
DOTNET_STARTUP_HOOKS, which any program can set for your account.) - Hidden desktops. XaultWallet refuses to start (exit code 4, a line in the log) on a desktop
other than the one you see (
WinSta0\Default): hidden-VNC malware starts its programs on a desktop of its own.
The costs: screen readers, voice control, password-manager auto-type and remote-control tools can't
operate XaultWallet while it is on. Turning it on asks for one click in the next 20 seconds; if the
click doesn't get through (an input device Windows doesn't report as one), Enhanced Security turns
itself off again, and it is kept on only once you click Keep it on (if XaultWallet closes first, it
starts without it and says so). If it stops accepting your input later (a remote-control tool, a
software KVM or a key remapper, for example), XaultWallet says so after a few ignored presses: close
XaultWallet from the taskbar and start it once with XaultWallet.exe --no-enhanced-security, or set
"EnhancedSecurity": false in settings.json in XaultWallet's data folder while it is closed.
Uninstalling keeps your settings, so reinstalling doesn't turn it off. Turning it off applies at once
to the window; the memory protection stays until the app restarts. Turning it on says which parts
Windows refused, if any.
What it is not: the setting lives in settings.json, which a program running as you can change before
the app starts; that program can also replace the app's files (including the libraries .NET unpacks
into your temp folder when XaultWallet starts), make Windows load its own code into XaultWallet, read
the vault file and log your password. It can also use the browser companion's channel to put a payment
in the Send form, which is why every payment waits for your click on Send now. Enhanced Security
takes away the easy ways common malware watches and drives a wallet. It can't make an infected computer
safe.
A remote/public node can see which blocks your wallet asks about and your broadcast
transactions' timing/origin. For maximum privacy run your own monerod, or route the daemon
connection over Tor: the built-in Tor (see above), or your own SOCKS proxy. Otherwise XaultWallet
passes your daemon address straight through to monero-wallet-rpc.
Encryption to the node. An http:// node address is unencrypted: anyone on the network path
sees (and can alter) the wallet's traffic with that node. The built-in presets are all http://.
For an https:// address the app starts monero-wallet-rpc with --daemon-ssl enabled --daemon-ssl-allow-any-cert (and asks for the same when you switch a wallet's node): the connection
must be TLS, never plain HTTP. Without it, wallet-rpc's default would fall back to plain HTTP if the
TLS handshake failed, so an attacker on the path could quietly remove the encryption. The node's
certificate is not verified: monero-wallet-rpc (v0.18.4) refuses --daemon-ssl enabled with
only system certificate authorities, and its CA-file option doesn't check the host name, so it
can't verify an https:// node the way a browser does (its default accepts an unverified
certificate too, with a log warning). This keeps the traffic encrypted against someone listening;
someone who can intercept the connection can still impersonate the node. Pinning the node's
certificate fingerprint would close that and isn't offered yet. A .onion node reached through
Tor (the built-in Tor, or your own SOCKS proxy) is encrypted and authenticated by Tor itself.
Through Tor or a proxy, monero-wallet-rpc refuses to open or create a wallet for any node it can't
verify (anything but an .onion or .i2p address with a port), unless it is told
--daemon-ssl-allow-any-cert. The app passes it with --proxy, so for a clearnet node the node's
certificate is not checked there either (as in wallet-rpc's default): Tor hides your IP address from
the node, but an http:// node's traffic is readable on its way from the exit relay to the node, and
nothing proves a clearnet node is the one you meant. Use an .onion node, or your own, for that.
A node also supplies the chain height a new seed starts scanning from. A height above the block
that holds a payment would hide that payment until the wallet is restored with an earlier height,
so the app never accepts more than the clock-based estimate monero's own wallet uses
(wallet2::get_approximate_blockchain_height), minus a one-day margin. A node can still lie in many
other ways (balances, history, fees); a node you don't run is trusted for all of them.
- Have the crypto core and duress logic independently audited.
- Test the full send/receive flow on stagenet first.
- Keep an offline backup of your 25-word seed. The vault is a convenience layer; the seed is the source of truth.
- Run under full-disk encryption.
Please do not open a public GitHub issue for security bugs. Public disclosure before a fix puts any user at risk.
Instead, report privately:
- Open a GitHub private security advisory (repo → Security → Report a vulnerability), or
- Contact the maintainer at https://dboudreau.dev.
Please include what you found, how to reproduce it, and the potential impact. As an unpaid solo/beta project there is no bug-bounty, but credit will be given (if you want it) once a fix is released. Please allow a reasonable window for a fix before any public disclosure.
Highest-priority areas: the vault format and its Argon2id/AES-256-GCM usage, the two-slot duress design (does it actually resist distinguishing the real slot from the decoy, including by someone holding the duress password?), the irreversible "wipe on duress" path, the local RPC authentication and port-ownership check, and how long the seed/password persist in process memory.
The October 2026 internal review and its fixes are documented in docs/AUDIT-2026-10.md.