feat: 提供加密备份文件解密脚本,用于离线恢复

This commit is contained in:
nap0o committed 2026-03-17 17:47:39 +08:00
1 parent d8daff4f0c
commit 023ea9564a
3 files changed
+132

No files matched your search

+12
View File
@@ -282,6 +282,18 @@ docker run -d --name 2fauth-worker \
<img height="400" src="https://github.com/user-attachments/assets/c6101ee8-f3c3-44f6-9286-f17865f8fb10" />
</details>
## 🔓 离线恢复 (Offline Recovery)
即使本项目停止运行,您也可以通过备份文件和您的加密密码找回 TOTP 原始数据。我们提供了一个独立的离线解密脚本,它不依赖于任何外部服务器:
- **脚本路径**:[scripts/decrypt_backup.js](file:///workspaces/2fa.hsiao.nom.za/scripts/decrypt_backup.js)
- **使用环境**:仅需安装 [Node.js](https://nodejs.org/) (内置 crypto 模块,无需 npm install)。
- **使用方法**:
```bash
node scripts/decrypt_backup.js <加密的备份文件.json> <您的密码>
```
解密后的数据将以明文 JSON 格式输出到控制台或保存到文件。
---
## 🔒 深度安全审计 (Security Auditing)
+12
View File
@@ -281,6 +281,18 @@ Regardless of the deployment method, these parameters are critical:
<img height="400" src="https://github.com/user-attachments/assets/c6101ee8-f3c3-44f6-9286-f17865f8fb10" />
</details>
## 🔓 Offline Recovery
Even if this project stops running, you can still recover your TOTP raw data using your backup file and encryption password. We provide a standalone offline decryption script that does not depend on any external servers:
- **Script Path**: [scripts/decrypt_backup.js](file:///workspaces/2fa.hsiao.nom.za/scripts/decrypt_backup.js)
- **Requirement**: Just [Node.js](https://nodejs.org/) (uses built-in crypto module, no `npm install` required).
- **Usage**:
```bash
node scripts/decrypt_backup.js <encrypted_backup.json> <your_password>
```
The decrypted data will be output to the console or saved to a file in plaintext JSON format.
---
## 🔒 Security Auditing
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* 2FAuth Backup Decryptor
* -----------------------
* A standalone script to decrypt 2FAuth backup files.
*
* Usage: node decrypt_backup.js <backup_file.json> <password>
*/
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
// Crypto Configuration (Must match 2FAuth standards)
const CRYPTO_CONFIG = {
ALGO_NAME: 'aes-256-gcm',
KDF_NAME: 'sha256',
ITERATIONS: 100000,
KEY_LEN: 32,
SALT_LEN: 16,
IV_LEN: 12,
TAG_LEN: 16
};
function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('\x1b[33m%s\x1b[0m', 'Usage: node decrypt_backup.js <path_to_backup.json> <password>');
process.exit(1);
}
const filePath = path.resolve(args[0]);
const password = args[1];
if (!fs.existsSync(filePath)) {
console.error('\x1b[31m%s\x1b[0m', `Error: File not found at ${filePath}`);
process.exit(1);
}
let backupContent;
try {
const fileData = fs.readFileSync(filePath, 'utf8');
backupContent = JSON.parse(fileData);
} catch (e) {
console.error('\x1b[31m%s\x1b[0m', `Error: Failed to read or parse JSON file: ${e.message}`);
process.exit(1);
}
if (!backupContent.encrypted || !backupContent.data) {
console.error('\x1b[31m%s\x1b[0m', 'Error: This file does not appear to be an encrypted 2FAuth backup or it is already decrypted.');
process.exit(1);
}
try {
const decryptedData = decrypt(backupContent.data, password);
const prettyJson = JSON.stringify(decryptedData, null, 2);
console.log('\x1b[32m%s\x1b[0m', '✅ Decryption Successful!');
console.log('\x1b[36m%s\x1b[0m', '--- Decrypted Content ---');
console.log(prettyJson);
console.log('\x1b[36m%s\x1b[0m', '-------------------------');
// Optional: Save to file
const outputFilename = `decrypted-${path.basename(filePath)}`;
fs.writeFileSync(outputFilename, prettyJson);
console.log('\x1b[32m%s\x1b[0m', `Result saved to: ${outputFilename}`);
} catch (e) {
console.error('\x1b[31m%s\x1b[0m', `❌ Decryption Failed: ${e.message}`);
console.error('\x1b[33m%s\x1b[0m', 'Check if your password is correct.');
process.exit(1);
}
}
/**
* Decrypts the base64 data string using the provided password
*/
function decrypt(base64Data, password) {
const combined = Buffer.from(base64Data, 'base64');
// Structure: [salt(16)] + [iv(12)] + [ciphertext(remaining - 16)] + [tag(16)]
// Note: Node.js decipher.setAuthTag requires the tag to be separated.
const salt = combined.subarray(0, CRYPTO_CONFIG.SALT_LEN);
const iv = combined.subarray(CRYPTO_CONFIG.SALT_LEN, CRYPTO_CONFIG.SALT_LEN + CRYPTO_CONFIG.IV_LEN);
const authTag = combined.subarray(combined.length - CRYPTO_CONFIG.TAG_LEN);
const ciphertext = combined.subarray(CRYPTO_CONFIG.SALT_LEN + CRYPTO_CONFIG.IV_LEN, combined.length - CRYPTO_CONFIG.TAG_LEN);
// Derive key
const key = crypto.pbkdf2Sync(
password,
salt,
CRYPTO_CONFIG.ITERATIONS,
CRYPTO_CONFIG.KEY_LEN,
CRYPTO_CONFIG.KDF_NAME
);
// Decrypt
const decipher = crypto.createDecipheriv(CRYPTO_CONFIG.ALGO_NAME, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, null, 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
main();