Skip to content

Instantly share code, notes, and snippets.

@cyio
Created June 3, 2025 01:59
Show Gist options
  • Save cyio/050c4689545ffca28fc1986a00d933eb to your computer and use it in GitHub Desktop.
Save cyio/050c4689545ffca28fc1986a00d933eb to your computer and use it in GitHub Desktop.
<!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