feat(config): 在加密导出/导入中包含个人访问令牌

This commit is contained in:
2026-03-23 17:59:23 +08:00
parent 0c7d2ad518
commit 8dd9e85b77
7 changed files with 787 additions and 48 deletions

View File

@@ -1,10 +1,10 @@
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use colored::Colorize;
use dialoguer::{Confirm, Input, Select, Password};
use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::config::{Language, manager::ConfigManager, ExportData, EncryptedPat};
use crate::config::CommitFormat;
use crate::utils::keyring::{get_supported_providers, get_default_model, get_default_base_url, provider_needs_api_key};
use crate::utils::crypto::{encrypt, decrypt};
@@ -777,9 +777,45 @@ impl ConfigCommand {
};
if pwd.is_empty() {
let mut has_pats = false;
for (profile_name, profile) in manager.config().profiles.iter() {
for service in profile.tokens.keys() {
if manager.has_pat_for_profile(profile_name, service) {
has_pats = true;
break;
}
}
}
if has_pats {
println!("{} {}", "".yellow(), "WARNING: Exporting without encryption.".bold());
println!(" {}", "Personal Access Tokens (PATs) stored in keyring will NOT be exported.".yellow());
println!(" {}", "To export PATs securely, please enable encryption.".yellow());
println!();
}
toml
} else {
let encrypted = encrypt(toml.as_bytes(), &pwd)?;
let mut encrypted_pats: Vec<EncryptedPat> = Vec::new();
for (profile_name, profile) in manager.config().profiles.iter() {
for service in profile.tokens.keys() {
if let Ok(Some(pat_value)) = manager.get_pat_for_profile(profile_name, service) {
let encrypted_token = encrypt(pat_value.as_bytes(), &pwd)?;
encrypted_pats.push(EncryptedPat {
profile_name: profile_name.clone(),
service: service.clone(),
user_email: profile.user_email.clone(),
encrypted_token,
});
}
}
}
let export_data = ExportData::with_encrypted_pats(toml, encrypted_pats);
let export_json = serde_json::to_string(&export_data)
.context("Failed to serialize export data")?;
let encrypted = encrypt(export_json.as_bytes(), &pwd)?;
format!("ENCRYPTED:{}", encrypted)
}
} else {
@@ -806,7 +842,7 @@ impl ConfigCommand {
async fn import_config(&self, file: &str, password: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let content = std::fs::read_to_string(file)?;
let config_content = if content.starts_with("ENCRYPTED:") {
let (config_content, encrypted_pats, pwd) = if content.starts_with("ENCRYPTED:") {
let encrypted_data = content.strip_prefix("ENCRYPTED:").unwrap();
let pwd = if let Some(p) = password {
@@ -817,21 +853,106 @@ impl ConfigCommand {
.interact()?
};
match decrypt(encrypted_data, &pwd) {
Ok(decrypted) => String::from_utf8(decrypted)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?,
let decrypted = match decrypt(encrypted_data, &pwd) {
Ok(d) => d,
Err(e) => {
bail!("Failed to decrypt configuration: {}. Please check your password.", e);
}
};
let decrypted_str = String::from_utf8(decrypted)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?;
match serde_json::from_str::<ExportData>(&decrypted_str) {
Ok(export_data) => {
(export_data.config, Some(export_data.encrypted_pats), Some(pwd))
}
Err(_) => {
(decrypted_str, None, Some(pwd))
}
}
} else {
content
(content, None, None)
};
let mut manager = self.get_manager(config_path)?;
manager.import(&config_content)?;
manager.save()?;
if let (Some(pats), Some(pwd)) = (encrypted_pats, pwd) {
if !pats.is_empty() {
println!();
println!("{}", "Importing Personal Access Tokens...".bold());
let mut imported_count = 0;
let mut failed_count = 0;
for pat in pats {
match decrypt(&pat.encrypted_token, &pwd) {
Ok(token_bytes) => {
match String::from_utf8(token_bytes) {
Ok(token_value) => {
if manager.keyring().is_available() {
match manager.store_pat_for_profile(
&pat.profile_name,
&pat.service,
&token_value
) {
Ok(_) => {
println!(" {} Token for {} ({}) imported to keyring",
"".green(),
pat.profile_name.cyan(),
pat.service.yellow());
imported_count += 1;
}
Err(e) => {
println!(" {} Failed to store token for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e);
failed_count += 1;
}
}
} else {
println!(" {} Keyring not available, cannot store token for {} ({})",
"".yellow(),
pat.profile_name,
pat.service);
failed_count += 1;
}
}
Err(e) => {
println!(" {} Invalid token format for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e);
failed_count += 1;
}
}
}
Err(e) => {
println!(" {} Failed to decrypt token for {} ({}): {}",
"".red(),
pat.profile_name,
pat.service,
e);
failed_count += 1;
}
}
}
println!();
if imported_count > 0 {
println!("{} {} token(s) imported to keyring", "".green(), imported_count);
}
if failed_count > 0 {
println!("{} {} token(s) failed to import", "".yellow(), failed_count);
}
}
}
println!("{} Configuration imported from {}", "".green(), file);
Ok(())
}