wizard: support key-based monero-wallet restore QR codes
What changed, and why it matters
This commit adds a feature to the Monero GUI wallet that lets users scan a QR code to restore a wallet from its secret keys. The change itself is a feature addition, not a direct security fix. However, it handles extremely sensitive data (private view and spend keys) encoded in QR codes, and the parsing logic uses string operations and URI decoding that could be risky if a malformed or malicious QR code is scanned. There is no evidence in the commit of input validation beyond checking the public address format. Because private keys are involved, any weakness in parsing could have serious consequences, but the commit does not demonstrate an actual vulnerability.
Treat this as a feature commit requiring security review rather than an active vulnerability. Reviewers should verify that scanned secret keys are validated (length, hex encoding), that URI parsing cannot be confused by embedded special characters, that decoded values are cleared from memory promptly, and that the QR scanner cannot be tricked into treating a payment QR as a wallet-restore QR. End users should ensure they only scan wallet-restore QR codes they generated themselves and keep them private.
Security signals we found
Parsing of URI-encoded secret key material from QR codes
Manual URI/query-string parsing instead of using a hardened parser
Use of decodeURIComponent on untrusted QR code data
Only address validity is checked; secret view/spend key values are not validated
Private spend key exposure via QR code is a high-sensitivity operation by design
Evidence from the diff
The commit introduces walletRestoreMode in QRCodeScanner.qml, a parseWalletRestoreUri() function, and wiring in WizardRestoreWallet1.qml. It changes the QR URI scheme from monero_wallet: to monero-wallet: in Keys.qml. The parser extracts view_key, spend_key, and height query parameters and emits them via the existing qrcode_decoded signal as extra_parameters. The code checks that the address is valid for the current network type, but does not validate the format or length of the secret keys before passing them onward. The parsing uses decodeURIComponent on query values and manual string splitting. A malformed QR code triggers an error notification and stops scanning. The commit is a feature patch, not a security patch, and no CVE or vendor security disclosure is referenced.
Changed components
components/QRCodeScanner.qmlpages/Keys.qmlwizard/WizardRestoreWallet1.qmlInspect captured patch +52 / −1
### components/QRCodeScanner.qml
@@ -45,6 +45,41 @@ Rectangle {
state: "Stopped"
signal qrcode_decoded(string address, string payment_id, string amount, string tx_description, string recipient_name, var extra_parameters)
+ property bool walletRestoreMode: false
+
+ function parseWalletRestoreUri(data) {
+ var prefix = ""
+ if (data.indexOf("monero-wallet:") === 0)
+ prefix = "monero-wallet:"
+ else if (data.indexOf("monero_wallet:") === 0)
+ prefix = "monero_wallet:"
+ else
+ return null
+
+ var queryOffset = data.indexOf("?")
+ var address = data.substring(prefix.length, queryOffset < 0 ? data.length : queryOffset)
+ if (!walletManager.addressValid(address, appWindow.persistentSettings.nettype))
+ return null
+
+ var params = {}
+ if (queryOffset >= 0) {
+ var items = data.substring(queryOffset + 1).split("&")
+ for (var index = 0; index < items.length; ++index) {
+ var separator = items[index].indexOf("=")
+ if (separator < 0)
+ continue
+ var name = decodeURIComponent(items[index].substring(0, separator))
+ var value = decodeURIComponent(items[index].substring(separator + 1))
+ if (name === "view_key")
+ params.secret_view_key = value
+ else if (name === "spend_key")
+ params.secret_spend_key = value
+ else if (name === "height")
+ params.restore_height = value
+ }
+ }
+ return { "address": address, "extra_parameters": params }
+ }
states: [
State {
@@ -67,6 +102,7 @@ Rectangle {
root.visible = false
finder.enabled = false
camera.cameraState = Camera.UnloadedState
+ root.walletRestoreMode = false
}
}
}
@@ -86,6 +122,19 @@ Rectangle {
id : finder
objectName: "QrFinder"
onDecoded : {
+ var walletRestore = null
+ try {
+ if (root.walletRestoreMode)
+ walletRestore = root.parseWalletRestoreUri(data)
+ } catch (error) {
+ finder.notifyError(qsTr("Invalid wallet restore QR code"), false)
+ return
+ }
+ if (walletRestore !== null) {
+ root.qrcode_decoded(walletRestore.address, "", "", "", "", walletRestore.extra_parameters)
+ root.state = "Stopped"
+ return
+ }
const parsed = walletManager.parse_uri_to_object(data);
if (!parsed.error) {
root.qrcode_decoded(parsed.address, parsed.payment_id, parsed.amount, parsed.tx_description, parsed.recipient_name, parsed.extra_parameters);
### pages/Keys.qml
@@ -281,7 +281,7 @@ Rectangle {
seedText.text = currentWallet.seed === "" ? qsTr("Mnemonic seed protected by hardware device.") + translationManager.emptyString : currentWallet.seed
if(typeof currentWallet != "undefined") {
- viewOnlyQRCode.source = "image://qrcode/monero_wallet:" + currentWallet.address(0, 0) + "?view_key="+currentWallet.secretViewKey+"&height="+currentWallet.walletCreationHeight
+ viewOnlyQRCode.source = "image://qrcode/monero-wallet:" + currentWallet.address(0, 0) + "?view_key="+currentWallet.secretViewKey+"&height="+currentWallet.walletCreationHeight
fullWalletQRCode.source = viewOnlyQRCode.source +"&spend_key="+currentWallet.secretSpendKey
if(currentWallet.viewOnly) {
### wizard/WizardRestoreWallet1.qml
@@ -75,6 +75,7 @@ Rectangle {
restoreHeight.text = typeof params.restore_height != "undefined" ? params.restore_height : "";
cameraUi.qrcode_decoded.disconnect(updateFromQrCode);
+ cameraUi.walletRestoreMode = false;
}
function verifyFromKeys() {
@@ -169,6 +170,7 @@ Rectangle {
text: FontAwesome.qrcode
tooltip: qsTr("Scan wallet QR code") + translationManager.emptyString
onClicked: {
+ cameraUi.walletRestoreMode = true;
cameraUi.qrcode_decoded.disconnect(updateFromQrCode);
cameraUi.qrcode_decoded.connect(updateFromQrCode);
cameraUi.state = "Capture";Why this scored 27/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.