feat: 提供加密备份文件解密脚本,用于离线恢复
This commit is contained in:
1 parent
d8daff4f0c
commit
023ea9564a
3 files changed
+132
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
Reference in new issue
Block a user