feat: add vaultwarden CLI and backup

This commit is contained in:
1803560007
2026-02-13 22:23:33 +08:00
parent efb50bed68
commit 03388ec6b4
3 changed files with 632 additions and 0 deletions

146
show_all.js Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
const fs = require('fs');
const https = require('https');
const CONFIG_PATH = process.env.HOME + '/.config/vaultwarden/config.json';
let config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
const VAULT_URL = config.url;
const CLIENT_ID = config.client_id;
const CLIENT_SECRET = config.client_secret;
let token = null;
let tokenExpiry = null;
const deviceId = 'openclaw-cli-' + require('crypto').randomUUID();
async function getToken() {
if (token && tokenExpiry && Date.now() < tokenExpiry - 300000) {
return token;
}
const params = new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
device_identifier: deviceId,
device_type: '14',
device_name: 'OpenClaw CLI'
});
const response = await new Promise((resolve, reject) => {
const url = new URL(VAULT_URL + '/identity/connect/token');
const options = {
hostname: url.hostname, port: url.port || 443,
path: url.pathname + url.search, method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 30000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const json = JSON.parse(body);
resolve(json);
} catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(params.toString());
req.end();
});
if (!response.access_token) {
throw new Error('认证失败');
}
token = response.access_token;
tokenExpiry = Date.now() + ((response.expires_in || 7200) - 300) * 1000;
return token;
}
async function getItem(id) {
const t = await getToken();
return new Promise((resolve, reject) => {
const url = new URL(VAULT_URL + `/api/ciphers/${id}`);
const options = {
hostname: url.hostname, port: url.port || 443,
path: url.pathname + url.search, method: 'GET',
headers: { 'Authorization': `Bearer ${t}` },
timeout: 30000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const json = JSON.parse(body);
resolve(json);
} catch (e) { reject(e); }
});
});
req.on('error', reject);
req.end();
});
}
async function getAllItems() {
const t = await getToken();
const response = await new Promise((resolve, reject) => {
const url = new URL(VAULT_URL + '/api/ciphers');
const options = {
hostname: url.hostname, port: url.port || 443,
path: url.pathname + url.search, method: 'GET',
headers: { 'Authorization': `Bearer ${t}` },
timeout: 30000
};
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const json = JSON.parse(body);
resolve(json);
} catch (e) { reject(e); }
});
});
req.on('error', reject);
req.end();
});
return response.data || [];
}
async function main() {
try {
const items = await getAllItems();
console.log(`\n📋 密码列表 (${items.length})\n`);
console.log('=' .repeat(70));
for (const item of items) {
const detail = await getItem(item.id);
console.log(`\n🔐 ${detail.name}`);
console.log('-'.repeat(50));
console.log(` ID: ${detail.id}`);
if (detail.login?.username) console.log(` 用户名: ${detail.login.username}`);
if (detail.login?.password) console.log(` 密码: ${detail.login.password}`);
if (detail.login?.uris?.length > 0) {
detail.login.uris.forEach(uri => {
console.log(` URL: ${uri.uri}`);
});
}
if (detail.notes) console.log(` 备注: ${detail.notes}`);
}
console.log('\n' + '='.repeat(70));
console.log(`\n✅ 共 ${items.length} 个密码\n`);
} catch (error) {
console.error('\n❌ 错误:', error.message);
process.exit(1);
}
}
main();

118
vaultwarden_backup.md Normal file
View File

@@ -0,0 +1,118 @@
# Vaultwarden 密码库备份
# 生成时间: 2026-02-13 22:14
---
## 坚果云 WebDAV
- **用户名:** work_fyx02@outlook.com
- **密码:** auyqxhk7fvhvhh3w
- **备注:** WebDAV password for jianguoyun.com
---
## Gitea Token
- **用户名:** fyx
- **密码:** 7840e0250de4cf994631b1eadb4fc469947aa7df
- **备注:** API Token for Gitea (162.211.228.232:8418)
---
## FreshRSS API
- **用户名:** 1803560007
- **密码:** asCdEfGhsasdasdavWxYz1234ass
- **备注:** API Token for FreshRSS (43.163.195.176:36847)
---
## 飞牛 NAS
- **用户名:** fyx
- **密码:** fengyaxing123
- **备注:** WebDAV for Feiniu NAS (f180356.5ddd.com)
---
## GitHub PAT
- **用户名:** 1803560007
- **密码:** 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
- **备注:** Personal Access Token for GitHub
---
## YouTube API
- **用户名:** API Key
- **密码:** AIzaSyC9HYKmkK6rSX1eyPwv3p3cQ4prGa2h-TE
- **备注:** API Key for YouTube Data API v3
---
## Bilibili UserID
- **用户名:** UserID
- **密码:** 356360432
- **备注:** B站用户ID DedeUserID
---
## Bilibili SESSDATA
- **用户名:** SESSDATA
- **密码:** 7a9023e2%2C1785783731%2Ce6963%2A21CjCO8WDiZbbX_EbrIi6niuZtTZMzxsl__Yt_Mo0qWXLZY-Pk9pxZ8qp_WxDjD3fdkfoSVlE1TllKNzIxTlVkam5VbVhzano4Q05hZGMtTWdUZ3lMNm1jdWFfdU1CWlY0dDVJU1BWNGI4V0pzN19ualZ2RjVrSHMyRjZlZk5uQWRTVEU5SXFjWXh3IIEC
- **备注:** B站登录凭证 SESSDATA
---
## qBittorrent
- **用户名:** 1803560007
- **密码:** fengyaxing123
- **备注:** Downloader on 162.211.228.232:8082
---
## 滴答清单 API
- **用户名:** Token
- **密码:** dp_9a8e7eccb01b44559e061dc58a669037
- **备注:** Access Token for Dida365
---
## Memos Access Token
- **用户名:** fyx
- **密码:** eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiZnl4IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NTY0MzI4N30.pIOySEM5VqYSTM-YLwcjiSMS4y10fbGrtTtm3afjjSI
- **备注:** Access Token for Memos (162.211.228.232:5230)
---
## 小红书
- **用户名:** session
- **密码:** 040069b97e1d876ba1a108d9ce3a4bf940ffca
- **备注:** 小红书 web_session
---
## Notion Integration
- **用户名:** Token
- **密码:** ntn_c43902219395mirQBetIfYoww1qKCAF14GBRUQeDee29o2
- **备注:** Integration Token for Notion API
---
## NewsAPI
- **用户名:** API Key
- **密码:** 744fb0c696f546cc95545974d18401bb
- **备注:** API Key for newsapi.org
---
## Cubox
- **用户名:** Token
- **密码:** aooyYG5itvB
- **备注:** API Token for Cubox
---
## 163邮箱 SMTP
- **用户名:** work_fyx02@163.com
- **密码:** PU7fV9D2UeVN9duK
- **备注:** SMTP授权码 for work_fyx02@163.com
---
*共 16 条记录 | 存储位置: Vaultwarden (bit.180356.xyz)*

368
vaultwarden_cli.js Normal file
View File

@@ -0,0 +1,368 @@
#!/usr/bin/env node
/**
* Vaultwarden CLI Tool (交互版)
* - 交互式输入密码
* - 支持查看密码
* - OAuth 自动认证
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const readline = require('readline');
// Configuration
const CONFIG_PATH = process.env.VAULTWARDEN_CONFIG_PATH ||
path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vaultwarden', 'config.json');
const ENV = {
url: process.env.VAULTWARDEN_URL || 'https://bit.180356.xyz',
clientId: process.env.VAULTWARDEN_CLIENT_ID || '',
clientSecret: process.env.VAULTWARDEN_CLIENT_SECRET || ''
};
// Load config file
let config = { url: ENV.url, client_id: '', client_secret: '' };
if (fs.existsSync(CONFIG_PATH)) {
try {
config = { ...config, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) };
} catch (e) {
console.error('Error reading config:', e.message);
}
}
const VAULT_URL = ENV.url || config.url;
const CLIENT_ID = ENV.clientId || config.client_id;
const CLIENT_SECRET = ENV.clientSecret || config.client_secret;
// 交互式输入
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function ask(question) {
return new Promise(resolve => {
rl.question(question, answer => resolve(answer));
});
}
class VaultwardenAPI {
constructor() {
this.token = null;
this.tokenExpiry = null;
this.deviceId = 'openclaw-cli-' + require('crypto').randomUUID();
this.deviceType = '14';
this.deviceName = 'OpenClaw CLI';
}
async getToken() {
if (this.token && this.tokenExpiry && Date.now() < this.tokenExpiry - 300000) {
return this.token;
}
const params = new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
device_identifier: this.deviceId,
device_type: this.deviceType,
device_name: this.deviceName
});
const response = await this.request(
'/identity/connect/token',
'POST',
params.toString(),
false,
'application/x-www-form-urlencoded'
);
if (!response.access_token) {
throw new Error('认证失败');
}
this.token = response.access_token;
this.tokenExpiry = Date.now() + ((response.expires_in || 7200) - 300) * 1000;
return this.token;
}
async request(endpoint, method = 'GET', data = null, useAuth = true, contentType = 'application/json') {
const url = new URL(VAULT_URL + endpoint);
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: method,
headers: {
'Content-Type': contentType,
'Accept': 'application/json'
},
timeout: 30000
};
if (useAuth && this.token) {
options.headers['Authorization'] = `Bearer ${this.token}`;
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const json = body ? JSON.parse(body) : {};
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(json);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${json.message || body}`));
}
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('超时')); });
if (data) req.write(data);
req.end();
});
}
async listItems() {
await this.getToken();
const response = await this.request('/api/ciphers');
return response.data || [];
}
async getItem(id) {
await this.getToken();
return await this.request(`/api/ciphers/${id}`);
}
async getItemByName(name) {
const items = await this.listItems();
return items.find(item => item.name?.toLowerCase() === name.toLowerCase());
}
async createItem(name, password, username = '', notes = '') {
await this.getToken();
const itemData = {
name: name,
notes: notes,
type: 1,
favorite: false,
login: {
username: username,
password: password,
totp: null
},
fields: [],
collectionIds: []
};
return await this.request('/api/ciphers', 'POST', JSON.stringify(itemData));
}
async deleteItem(id) {
await this.getToken();
return await this.request(`/api/ciphers/${id}?permanent=true`, 'DELETE');
}
}
async function main() {
const args = process.argv.slice(2);
const command = args[0] || 'list';
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error('❌ 未配置! 请设置环境变量或配置文件');
console.log('\n💡 运行: npm run setup');
process.exit(1);
}
const api = new VaultwardenAPI();
try {
switch (command) {
case 'save':
case 'add':
await handleSave(args.slice(1), api);
break;
case 'list':
await handleList(api);
break;
case 'get':
await handleGet(args.slice(1), api);
break;
case 'search':
await handleSearch(args.slice(1), api);
break;
case 'delete':
await handleDelete(args.slice(1), api);
break;
case 'setup':
await handleSetup();
break;
case 'help':
default:
showHelp();
break;
}
} catch (error) {
console.error('\n❌ 错误:', error.message);
process.exit(1);
} finally {
rl.close();
}
}
async function handleSave(args, api) {
console.log('\n📝 保存新项目\n');
const name = await ask('名称: ');
if (!name) {
console.error('❌ 名称不能为空');
return;
}
const username = await ask('用户名 (可选): ');
const password = await ask('密码: ');
if (!password) {
console.error('❌ 密码不能为空');
return;
}
const notes = await ask('备注 (可选): ');
await api.createItem(name, password, username, notes);
console.log(`\n✅ 已保存: ${name}\n`);
}
async function handleList(api) {
const items = await api.listItems();
console.log(`\n📋 项目列表 (${items.length})\n`);
items.forEach(item => {
const name = item.name || 'Unknown';
const id = item.id?.slice(0, 8) || 'N/A';
console.log(`${name} [${id}]`);
});
console.log('');
}
async function handleGet(args, api) {
const name = args[0];
if (!name) {
console.error('\n用法: get <名称>\n');
return;
}
const item = await api.getItemByName(name);
if (!item) {
console.error(`\n❌ 未找到: ${name}\n`);
return;
}
console.log(`\n📄 ${item.name}\n`);
console.log(` ID: ${item.id}`);
console.log(` 用户名: ${item.login?.username || '无'}`);
console.log(` 密码: ${item.login?.password || '无'}`);
console.log(` 备注: ${item.notes || '无'}`);
console.log('');
}
async function handleSearch(args, api) {
const query = args[0];
if (!query) {
console.error('\n用法: search <关键词>\n');
return;
}
const items = await api.listItems();
const results = items.filter(item =>
item.name?.toLowerCase().includes(query.toLowerCase()) ||
item.login?.username?.toLowerCase().includes(query.toLowerCase()) ||
item.notes?.toLowerCase().includes(query.toLowerCase())
);
console.log(`\n🔍 搜索 "${query}" (${results.length} 个结果)\n`);
results.forEach(item => {
console.log(`${item.name}`);
});
console.log('');
}
async function handleDelete(args, api) {
const name = args[0];
if (!name) {
console.error('\n用法: delete <名称>\n');
return;
}
const item = await api.getItemByName(name);
if (!item) {
console.error(`\n❌ 未找到: ${name}\n`);
return;
}
const answer = await ask(`🗑️ 删除 "${item.name}"? (y/N): `);
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
console.log('已取消\n');
return;
}
await api.deleteItem(item.id);
console.log(`\n✅ 已删除: ${item.name}\n`);
}
async function handleSetup() {
console.log('\n🔐 Vaultwarden 配置\n');
const url = await ask('服务器地址 (默认 https://bit.180356.xyz): ');
const clientId = await ask('Client ID: ');
const clientSecret = await ask('Client Secret: ');
const configData = {
url: url || 'https://bit.180356.xyz',
client_id: clientId,
client_secret: clientSecret
};
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
fs.writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
console.log('\n✅ 配置已保存!\n');
}
function showHelp() {
console.log(`
🔐 Vaultwarden CLI (交互版)
用法:
save 保存新项目 (交互式)
list 列出所有项目
get <名称> 获取项目详情
search <关键词> 搜索项目
delete <名称> 删除项目
setup 配置
help 显示帮助
示例:
save # 交互式保存
list # 列出所有
get GitHub # 获取 GitHub
search GitHub # 搜索
delete GitHub # 删除
注意: 密码会在命令行中显示
`);
}
module.exports = {};
if (require.main === module) {
main();
}