Created
June 3, 2025 01:59
-
-
Save cyio/050c4689545ffca28fc1986a00d933eb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="zh-CN"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>BitSPV Wallet Backup Decryption Tool</title> | |
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'none';"> | |
<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.js"></script> | |
<style> | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
line-height: 1.6; | |
color: #333; | |
max-width: 800px; | |
margin: 0 auto; | |
padding: 20px; | |
background-color: #f5f5f5; | |
} | |
h1 { | |
text-align: center; | |
color: #2563eb; | |
margin-bottom: 20px; | |
} | |
.container { | |
background-color: white; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); | |
padding: 20px; | |
margin-bottom: 20px; | |
} | |
.form-group { | |
margin-bottom: 20px; | |
} | |
label { | |
display: block; | |
margin-bottom: 8px; | |
font-weight: 600; | |
} | |
textarea, input { | |
width: 100%; | |
padding: 10px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
font-size: 16px; | |
box-sizing: border-box; | |
} | |
textarea { | |
min-height: 120px; | |
resize: vertical; | |
} | |
button { | |
background-color: #2563eb; | |
color: white; | |
border: none; | |
padding: 12px 20px; | |
border-radius: 4px; | |
font-size: 16px; | |
cursor: pointer; | |
width: 100%; | |
transition: background-color 0.2s; | |
} | |
button:hover { | |
background-color: #1d4ed8; | |
} | |
button:disabled { | |
background-color: #93c5fd; | |
cursor: not-allowed; | |
} | |
.result { | |
display: none; | |
margin-top: 20px; | |
} | |
.result.visible { | |
display: block; | |
} | |
.result-section { | |
margin-bottom: 15px; | |
} | |
.result-title { | |
font-weight: 600; | |
margin-bottom: 5px; | |
} | |
.result-content { | |
background-color: #f8f9fa; | |
padding: 10px; | |
border-radius: 4px; | |
border: 1px solid #eee; | |
overflow-wrap: break-word; | |
word-break: break-all; /* Ensure long words or URLs break as needed */ | |
white-space: pre-wrap; /* Preserve whitespace and newlines, while allowing automatic wrapping */ | |
font-family: monospace; | |
} | |
.error { | |
color: #dc2626; | |
margin-top: 5px; | |
font-size: 14px; | |
} | |
.success { | |
color: #16a34a; | |
} | |
.warning { | |
background-color: #fff7ed; | |
border: 1px solid #ffedd5; | |
border-radius: 4px; | |
padding: 10px; | |
margin-bottom: 20px; | |
color: #c2410c; | |
} | |
#qr-scanner-container { | |
margin-top: 20px; | |
text-align: center; | |
} | |
#qr-video { | |
width: 100%; | |
max-width: 400px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
display: block; | |
margin: 0 auto; | |
} | |
#qr-canvas { | |
display: none; /* Hidden canvas, only used for image processing */ | |
} | |
#scan-btn { | |
margin-top: 10px; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>BitSPV Wallet Backup Validation and Decryption Tool</h1> | |
<p style="text-align: center; margin-top: -10px; margin-bottom: 20px; color: #555;"> | |
This tool serves two primary functions: <strong>validating the effectiveness of your wallet backup file</strong> and <strong>decrypting your private key</strong>. | |
</p> | |
<div class="warning"> | |
<strong>Security Warning:</strong> This tool is for emergency wallet recovery only. Please ensure you use it in a secure environment. Private key exposure may lead to loss of funds! | |
</div> | |
<div class="container"> | |
<div class="form-group"> | |
<label for="qr-file-input">Open Wallet Backup File (QR Code Image)</label> | |
<input type="file" id="qr-file-input" accept="image/*"> | |
<div id="qr-scan-error" class="error"></div> | |
<canvas id="qr-canvas" style="display:none;"></canvas> <!-- Hidden canvas, used for image processing --> | |
</div> | |
<div class="form-group"> | |
<label for="backup-data">Or Wallet Backup Data (JSON Format)</label> | |
<textarea id="backup-data" placeholder="Paste wallet backup JSON data..."></textarea> | |
<div id="backup-error" class="error"></div> | |
</div> | |
<div class="form-group"> | |
<label for="pin">PIN Code (required only for private key decryption)</label> | |
<input type="password" id="pin" placeholder="Enter your wallet PIN code"> | |
<div id="pin-error" class="error"></div> | |
</div> | |
<button id="decrypt-btn">Decrypt Private Key</button> | |
</div> | |
<div id="result" class="container result"> | |
<h2>Decryption Result</h2> | |
<div class="result-section"> | |
<div class="result-title">Raw JSON Data:</div> | |
<pre id="json-result" class="result-content"></pre> | |
</div> | |
<div class="result-section"> | |
<div class="result-title">Wallet Address:</div> | |
<div id="address-result" class="result-content"></div> | |
</div> | |
<div class="result-section"> | |
<div class="result-title">Wallet Name:</div> | |
<div id="name-result" class="result-content"></div> | |
</div> | |
<div class="result-section"> | |
<div class="result-title">Creation Time:</div> | |
<div id="createdAt-result" class="result-content"></div> | |
</div> | |
<div class="result-section" id="wif-section" style="display:none;"> | |
<div class="result-title">Private Key (WIF Format):</div> | |
<div id="wif-result" class="result-content"></div> | |
</div> | |
</div> | |
<script> | |
// Utility functions copied from webauthn.js | |
function arrayBufferToBase64Url(buffer) { | |
return btoa(String.fromCharCode(...new Uint8Array(buffer))) | |
.replace(/\+/g, '-') | |
.replace(/\//g, '_') | |
.replace(/=+$/, ''); | |
} | |
function base64UrlToArrayBuffer(base64Url) { | |
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | |
const binaryString = atob(base64); | |
const len = binaryString.length; | |
const bytes = new Uint8Array(len); | |
for (let i = 0; i < len; i++) { | |
bytes[i] = binaryString.charCodeAt(i); | |
} | |
return bytes.buffer; | |
} | |
async function deriveKeyFromPin(pin, salt) { | |
const encoder = new TextEncoder(); | |
const keyMaterial = await crypto.subtle.importKey( | |
'raw', | |
encoder.encode(pin), | |
{ name: 'PBKDF2' }, | |
false, | |
['deriveKey'] | |
); | |
return crypto.subtle.deriveKey( | |
{ | |
name: 'PBKDF2', | |
salt: salt, | |
iterations: 100000, | |
hash: 'SHA-256', | |
}, | |
keyMaterial, | |
{ name: 'AES-GCM', length: 256 }, | |
true, | |
['encrypt', 'decrypt'] | |
); | |
} | |
async function decryptData(ciphertextB64Url, ivB64Url, saltB64Url, pin) { | |
try { | |
const salt = base64UrlToArrayBuffer(saltB64Url); | |
const iv = base64UrlToArrayBuffer(ivB64Url); | |
const key = await deriveKeyFromPin(pin, salt); | |
const ciphertext = base64UrlToArrayBuffer(ciphertextB64Url); | |
const decryptedData = await crypto.subtle.decrypt( | |
{ | |
name: 'AES-GCM', | |
iv: iv, | |
}, | |
key, | |
ciphertext | |
); | |
const decoder = new TextDecoder(); | |
return decoder.decode(decryptedData); | |
} catch (error) { | |
console.error('Decryption failed:', error); | |
return null; | |
} | |
} | |
// Page interaction logic | |
document.addEventListener('DOMContentLoaded', function() { | |
const backupDataInput = document.getElementById('backup-data'); | |
const pinInput = document.getElementById('pin'); | |
const decryptBtn = document.getElementById('decrypt-btn'); | |
const resultContainer = document.getElementById('result'); | |
const jsonResult = document.getElementById('json-result'); | |
const addressResult = document.getElementById('address-result'); | |
const nameResult = document.getElementById('name-result'); | |
const createdAtResult = document.getElementById('createdAt-result'); // Added | |
const wifResult = document.getElementById('wif-result'); | |
const backupError = document.getElementById('backup-error'); | |
const pinError = document.getElementById('pin-error'); | |
const qrFileInput = document.getElementById('qr-file-input'); | |
const qrCanvas = document.getElementById('qr-canvas'); | |
const qrScanError = document.getElementById('qr-scan-error'); | |
const qrCanvasCtx = qrCanvas.getContext('2d'); | |
// Function to check PIN and toggle decrypt button state | |
function checkPinAndToggleDecryptButton() { | |
decryptBtn.disabled = pinInput.value.trim() === ''; | |
} | |
// Initial check when the page loads | |
checkPinAndToggleDecryptButton(); | |
// Listen for input changes on PIN input to enable/disable the decrypt button | |
pinInput.addEventListener('input', checkPinAndToggleDecryptButton); | |
qrFileInput.addEventListener('change', function(event) { | |
qrScanError.textContent = ''; | |
const file = event.target.files[0]; | |
if (!file) { | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
const img = new Image(); | |
img.onload = function() { | |
qrCanvas.width = img.width; | |
qrCanvas.height = img.height; | |
qrCanvasCtx.drawImage(img, 0, 0, img.width, img.height); | |
const imageData = qrCanvasCtx.getImageData(0, 0, qrCanvas.width, qrCanvas.height); | |
const code = jsQR(imageData.data, imageData.width, imageData.height, { | |
inversionAttempts: 'dontInvert', | |
}); | |
if (code) { | |
console.log('QR code decoded successfully:', code.data); | |
backupDataInput.value = code.data; | |
qrScanError.textContent = 'QR code scanned successfully!'; | |
// Attempt to parse and display wallet information | |
try { | |
const backupData = JSON.parse(code.data); | |
jsonResult.textContent = JSON.stringify(backupData, null, 2); | |
addressResult.textContent = backupData.address || 'Unknown'; | |
nameResult.textContent = backupData.walletName || 'Unnamed Wallet'; | |
createdAtResult.textContent = backupData.timestamp ? new Date(backupData.timestamp).toLocaleString() : 'Unknown'; | |
resultContainer.classList.add('visible'); // Display result container | |
document.getElementById('wif-section').style.display = 'none'; // Hide private key section | |
} catch (e) { | |
qrScanError.textContent = 'QR code content is not valid JSON format.'; | |
resultContainer.classList.remove('visible'); // Hide result container | |
} | |
} else { | |
qrScanError.textContent = 'Could not detect QR code. Please ensure the image is clear.'; | |
resultContainer.classList.remove('visible'); // Hide result container | |
} | |
}; | |
img.onerror = function() { | |
qrScanError.textContent = 'Could not load image. Please ensure the file is a valid image format.'; | |
resultContainer.classList.remove('visible'); // Hide result container | |
}; | |
img.src = e.target.result; | |
}; | |
reader.readAsDataURL(file); | |
}); | |
// Listen for input changes on backupDataInput so that information can be displayed immediately when pasting JSON manually | |
backupDataInput.addEventListener('input', function() { | |
backupError.textContent = ''; | |
resultContainer.classList.remove('visible'); | |
document.getElementById('wif-section').style.display = 'none'; // Hide private key section | |
const backupDataText = backupDataInput.value.trim(); | |
if (backupDataText) { | |
try { | |
const backupData = JSON.parse(backupDataText); | |
jsonResult.textContent = JSON.stringify(backupData, null, 2); | |
addressResult.textContent = backupData.address || 'Unknown'; | |
nameResult.textContent = backupData.walletName || 'Unnamed Wallet'; | |
createdAtResult.textContent = backupData.createdAt ? new Date(backupData.createdAt).toLocaleString() : 'Unknown'; | |
resultContainer.classList.add('visible'); // Display result container | |
} catch (e) { | |
backupError.textContent = 'Invalid JSON format. Please check backup data.'; | |
resultContainer.classList.remove('visible'); // Hide result container | |
} | |
} | |
}); | |
decryptBtn.addEventListener('click', async function() { | |
// Reset error messages | |
pinError.textContent = ''; | |
// Get input values | |
const backupDataText = backupDataInput.value.trim(); | |
const pin = pinInput.value.trim(); | |
// Validate input | |
let isValid = true; | |
if (!backupDataText) { | |
backupError.textContent = 'Please enter wallet backup data'; | |
isValid = false; | |
} else { | |
backupError.textContent = ''; // Clear previous error message | |
} | |
if (!pin) { | |
pinError.textContent = 'Please enter PIN code'; | |
isValid = false; | |
} | |
if (!isValid) return; | |
// Parse backup data (parsing again here to ensure data is up-to-date and to check encryption info) | |
let backupData; | |
try { | |
backupData = JSON.parse(backupDataText); | |
if (!backupData.encryptedWif || !backupData.iv || !backupData.salt) { | |
backupError.textContent = 'Invalid backup data format, missing required encryption information'; | |
return; | |
} | |
} catch (e) { | |
backupError.textContent = 'Invalid JSON format. Please check backup data.'; | |
return; | |
} | |
// Disable button, show loading state | |
decryptBtn.disabled = true; | |
decryptBtn.textContent = 'Decrypting...'; | |
try { | |
// Decrypt private key | |
const decryptedWif = await decryptData( | |
backupData.encryptedWif, | |
backupData.iv, | |
backupData.salt, | |
pin | |
); | |
if (!decryptedWif) { | |
pinError.textContent = 'Incorrect PIN code or decryption failed'; | |
decryptBtn.disabled = false; | |
decryptBtn.textContent = 'Decrypt Private Key'; | |
return; | |
} | |
// Display private key | |
wifResult.textContent = decryptedWif; | |
document.getElementById('wif-section').style.display = 'block'; // Display private key section | |
} catch (error) { | |
console.error('Decryption process error:', error); | |
pinError.textContent = `Decryption failed: ${error.message || 'Unknown error'}`; | |
} finally { | |
// Restore button state | |
decryptBtn.disabled = false; | |
decryptBtn.textContent = 'Decrypt Private Key'; | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment