feat(config): 在加密导出/导入中包含个人访问令牌
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user