fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(|s| s.as_str()) {
Some("add-account") => {
if let Err(e) = cli_add_account(&args[1..]) {
eprintln!("add-account 失败: {e}");
std::process::exit(1);
}
}
Some("list-accounts") => cli_list_accounts(),
Some("export-bundle") => {
if let Err(e) = cli_export_bundle(&args[1..]) {
eprintln!("export-bundle 失败: {e}");
std::process::exit(1);
}
}
Some("import-bundle") => {
if let Err(e) = cli_import_bundle(&args[1..]) {
eprintln!("import-bundle 失败: {e}");
std::process::exit(1);
}
}
Some("remove-account") => {
if let Err(e) = cli_remove_account(&args[1..]) {
eprintln!("remove-account 失败: {e}");
std::process::exit(1);
}
}
Some("secret-check") => {
if let Err(e) = cli_secret_check(&args[1..]) {
eprintln!("secret-check 失败: {e}");
std::process::exit(1);
}
}
Some("tq-secret") => {
if let Err(e) = cli_tq_secret(&args[1..]) {
eprintln!("tq-secret 失败: {e}");
std::process::exit(1);
}
}
Some("init-master-key") => {
if let Err(e) = cli_init_master_key() {
eprintln!("init-master-key 失败: {e}");
std::process::exit(1);
}
}
Some("rekey-secrets") => {
if let Err(e) = cli_rekey_secrets() {
eprintln!("rekey-secrets 失败: {e}");
std::process::exit(1);
}
}
Some("encrypt-blobs") => {
if let Err(e) = cli_encrypt_blobs() {
eprintln!("encrypt-blobs 失败: {e}");
std::process::exit(1);
}
}
Some("migrate-db") => {
if let Err(e) = cli_migrate_db() {
eprintln!("migrate-db 失败: {e}");
std::process::exit(1);
}
}
Some("help" | "--help" | "-h") => println!("{}", usage_screen()),
Some(other) => {
eprintln!("未知子命令 `{other}`。\n{}", usage_screen());
std::process::exit(2);
}
None => desktop_main(),
}
}
fn usage_screen() -> String {
"用法:
ekapp # 桌面主窗(勾选组件 → 启动运行)
ekapp add-account … # 无界面写档(密码走 stdin,不进 argv)
ekapp list-accounts # 已存档案清点(只读元信息,零密码值)
ekapp remove-account <ns> # 删户:段行 + 该段全部密文(幂等)
ekapp secret-check <ns> [字段] # 密库引用自检(只报在/缺与长度,零值)
ekapp tq-secret [--check] # 天勤桥凭据入密库(stdin 两行;--check 只打形状)
ekapp export-bundle <文件> # 整库导出(秘值用口令包裹,不含主密钥)
ekapp import-bundle <文件> [根] # 导入包(秘值改本机主钥重加密;根缺省 = eco)
ekapp init-master-key # 生成主密钥(已存在即拒,值不回显)
ekapp encrypt-blobs # 明文遗留就地加密(幂等)
ekapp rekey-secrets # 换钥修复(旧钥经 EK_OLD_MASTER_KEY=64 位 hex)
ekapp migrate-db # 一次性迁移 *.ini → cfg.db(幂等,旧档改名存档)
ekapp help # 本屏
口令纪律:凡口令一律 stdin / 环境变量,永不进 argv(进程行能被旁人看到)。"
.to_string()
}
fn desktop_main() {
if let Ok(st) = ek_cfg::Store::eco() {
let pending = st.legacy_pending();
if !pending.is_empty() {
eprintln!(
"ekapp: 根内发现未迁旧档 {} 个段({});单模下不可见 —— 请先跑 `ekapp migrate-db`",
pending.len(),
pending.join(", ")
);
}
}
if let Err(e) = ek_ui::runtime::run_desktop() {
eprintln!("ekapp: {e}");
std::process::exit(1);
}
}
fn usage() -> String {
"用法:
ekapp add-account --broker 9999 --user 278153 \
--td tcp://182.254.243.31:30001 --md tcp://182.254.243.31:30011 \
--app-id simnow_client_test --auth-code 0000000000000000 \
[--classes 1(只期货;现时唯一合法档)] \
[--md-user u --md-password-stdin]
密码在提示后从 stdin 读一行(不进 argv;进程行能被旁人看到)。"
.to_string()
}
fn cli_add_account(rest: &[String]) -> Result<(), String> {
let mut a = ek_ui::appconfig::AcctSpec {
broker_id: String::new(),
user: String::new(),
td_addr: String::new(),
md_addr: String::new(),
app_id: String::new(),
auth_code: String::new(),
classes: "1".to_string(),
md_user: String::new(),
md_password: None,
};
let mut i = 0;
while i < rest.len() {
let flag = rest[i].as_str();
let need = |i: &mut usize, rest: &[String], what: &str| -> Result<String, String> {
*i += 1;
rest.get(*i)
.cloned()
.ok_or_else(|| format!("{what} 缺值(参数后要跟值)"))
};
match flag {
"--broker" => a.broker_id = need(&mut i, rest, "--broker")?,
"--user" => a.user = need(&mut i, rest, "--user")?,
"--td" => a.td_addr = need(&mut i, rest, "--td")?,
"--md" => a.md_addr = need(&mut i, rest, "--md")?,
"--app-id" => a.app_id = need(&mut i, rest, "--app-id")?,
"--auth-code" => a.auth_code = need(&mut i, rest, "--auth-code")?,
"--classes" => a.classes = need(&mut i, rest, "--classes")?,
"--md-user" => a.md_user = need(&mut i, rest, "--md-user")?,
"--md-password-stdin" => a.md_password = Some(String::new()),
other => {
return Err(format!("未知参数 `{other}`\n{}", usage()));
}
}
i += 1;
}
let guid = ek_ui::appconfig::derived_name(&a)?;
eprint!("[ekapp] 输入 {guid} 的 CTP 密码(stdin 单行): ");
let mut buf = String::new();
std::io::stdin()
.read_line(&mut buf)
.map_err(|e| format!("stdin 读失败: {e}"))?;
let password = buf.trim().to_string();
if password.is_empty() {
return Err("密码为空:档案不接受无密码账户(受理校验同款)".to_string());
}
if a.md_password.is_some() {
eprint!("[ekapp] 输入 {guid} 的行情密码(stdin 单行;留空回落 td 密码): ");
let mut mbuf = String::new();
std::io::stdin()
.read_line(&mut mbuf)
.map_err(|e| format!("stdin 读失败: {e}"))?;
let mp = mbuf.trim().to_string();
a.md_password = (!mp.is_empty()).then_some(mp);
}
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let gw = ek_ui::appconfig::write_account(&store, &a, &password)?;
let md_txt = if a.md_addr.is_empty() {
"—未配—".to_string()
} else {
a.md_addr.clone()
};
println!(
"[ekapp] 已写档 ctp.{gw}.ini(td={td};md={md};密码=密库引用,值不落盘)。
下一步:启动 `ekapp`(桌面)点该行的『连接』;或起 `ek-station` 守护(自动连接先在桌面行上勾)。",
td = a.td_addr,
md = md_txt
);
Ok(())
}
fn cli_remove_account(rest: &[String]) -> Result<(), String> {
let Some(ns) = rest.first() else {
return Err("用法: ekapp remove-account <ns>".to_string());
};
if ns == "ui" {
return Err("ui 段是界面偏好(主题/别名),不是账户,拒删;要清就手工编辑".to_string());
}
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let existed = matches!(store.load(ns), Ok(ek_cfg::LoadOutcome::Loaded { .. }));
if !existed {
println!("remove-account:段 {ns} 本来就没有(幂等,无动作)");
return Ok(());
}
store.purge(ns).map_err(|e| format!("抹段显形: {e}"))?;
let after = matches!(store.load(ns), Ok(ek_cfg::LoadOutcome::Loaded { .. }));
if after {
return Err(format!("抹段后仍读得到 {ns}(库写未落地,查 WAL/磁盘)"));
}
println!("remove-account 完成:{ns} 段行 + 该段密文已抹净,回读已确认不存在。");
Ok(())
}
fn cli_export_bundle(rest: &[String]) -> Result<(), String> {
let Some(path) = rest.first() else {
return Err("用法: ekapp export-bundle <文件> (口令从 stdin 读一行)".to_string());
};
println!("输入导出口令(≥8 字符,只在本进程内用,不落盘/不回显):");
let pass = read_stdin_line().ok_or("stdin 缺口令行")?;
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let pack =
ek_cfg::bundle::export(store.root(), pass.trim()).map_err(|e| format!("导出显形: {e}"))?;
std::fs::write(path, &pack).map_err(|e| format!("写包失败 {path}: {e}"))?;
println!(
"export-bundle 完成:{} 字节 → {path}(段面 + 秘值;秘值用口令包裹,不含主密钥)",
pack.len()
);
Ok(())
}
fn cli_import_bundle(rest: &[String]) -> Result<(), String> {
let Some(path) = rest.first() else {
return Err("用法: ekapp import-bundle <文件> [根] (口令从 stdin 读一行)".to_string());
};
println!("输入导入口令(导出时那个):");
let pass = read_stdin_line().ok_or("stdin 缺口令行")?;
let root = match rest.get(1) {
Some(r) => std::path::PathBuf::from(r),
None => ek_cfg::Store::eco()
.map_err(|e| format!("配置根不可用: {e:?}"))?
.root()
.to_path_buf(),
};
let data = std::fs::read(path).map_err(|e| format!("读包失败 {path}: {e}"))?;
let rep =
ek_cfg::bundle::import(&root, pass.trim(), &data).map_err(|e| format!("导入显形: {e}"))?;
println!(
"import-bundle 完成:{} 段 / {} 键 / {} 秘值(秘值已用本机主密钥重加密);根 = {}",
rep.sections,
rep.keys,
rep.secrets,
root.display()
);
Ok(())
}
fn cli_list_accounts() {
let store = match ek_cfg::Store::eco() {
Ok(s) => s,
Err(e) => {
eprintln!("[ekapp] 配置根不可用: {e:?}");
std::process::exit(1);
}
};
let nss = match store.list_namespaces() {
Ok(v) => v,
Err(e) => {
eprintln!("[ekapp] 档案目录列举失败: {e:?}");
eprintln!("[ekapp] (这不是「没有账户」,是库读不开 —— 查 WAL/权限/路径)");
std::process::exit(1);
}
};
let mut shown = 0usize;
for ns in &nss {
if !ns.starts_with("ctp.") {
continue;
}
shown += 1;
match store.load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => {
let p = |k: &str| map.get_plain(k).unwrap_or("—").to_string();
let has_pw = map
.get_secret_key("td_password")
.is_some_and(|k| !k.is_empty());
println!(
"{}\n 账号 {} @ broker {}\n td = {}\n md = {}\n 凭据 = {} | 收录 = {}",
ns,
p("td_user"),
p("broker_id"),
p("td_addr"),
p("md_addr"),
if has_pw {
"密库引用 ✓"
} else {
"缺密码引用"
},
p("product_classes"),
);
}
Ok(_) => println!("{ns}(一段都读不出;该档恐已坏)"),
Err(e) => println!("{ns}(读失败:{e:?})"),
}
}
if shown == 0 {
println!("[ekapp] 档案清点:无 ctp.* 账户段(空白首启)");
}
}
fn cli_secret_check(rest: &[String]) -> Result<(), String> {
use ek_cfg::secret;
let Some(ns) = rest.first() else {
return Err("用法: ekapp secret-check <ns> [字段=td_password]".to_string());
};
let field = rest.get(1).map(|s| s.as_str()).unwrap_or("td_password");
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let map = match store.load(ns) {
Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => map,
Ok(_) => return Err(format!("{ns}: 档案读不出(段坏/不存在)")),
Err(e) => return Err(format!("{ns}: 档案读失败 {e:?}")),
};
let Some(combo) = map.get_secret_key(field) else {
return Err(format!("{ns}.{field}: 无密库引用(明文键或键不存在)"));
};
println!("{ns}.{field} → 引用 combo 键 {combo:?}");
match secret::get(ns, combo) {
Some(v) => println!("库值:在(长度 {} 字节)", v.as_bytes().len()),
None => println!("库值:缺(引用在,密库无条目 —— 编辑弹窗重输后『保存并连接』补录)"),
}
Ok(())
}
fn cli_init_master_key() -> Result<(), String> {
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let (_mk, path) =
ek_cfg::MasterKey::write_new(store.root()).map_err(|e| format!("生成主密钥显形: {e}"))?;
println!(
"init-master-key 完成:{} ({}B,弱钥校验通过;值不回显、不进日志)。\n\
纪律:该文件是**唯一门槛** —— 丢了 = 库里秘值不可解(需重灌口令);\n\
备份建议:离线介质另存,勿入 git(仓内禁 *.key / cfg.db 已由闸口守)。",
path.display(),
ek_cfg::crypto::MASTER_KEY_LEN
);
Ok(())
}
fn cli_rekey_secrets() -> Result<(), String> {
use ek_cfg::crypto::MasterKey;
let hex = std::env::var("EK_OLD_MASTER_KEY")
.map_err(|_| "缺 EK_OLD_MASTER_KEY(64 位 hex 旧主钥)".to_string())?;
let raw = (0..hex.len() / 2)
.map(|i| u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16))
.collect::<Result<Vec<u8>, _>>()
.map_err(|_| "EK_OLD_MASTER_KEY 不是合法 hex".to_string())?;
let old = MasterKey::from_bytes(&raw).map_err(|e| format!("旧主钥非法: {e}"))?;
let root = ek_cfg::Store::default_root().ok_or("取不到配置根(APPDATA/ek 缺席)")?;
let (total, fixed) = ek_cfg::cfg_rekey_blobs(&root, &old).map_err(|e| e.to_string())?;
println!("rekey: 共 {total} 条密文,重封 {fixed} 条");
Ok(())
}
fn cli_encrypt_blobs() -> Result<(), String> {
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let (done, skipped) =
ek_cfg::cfg_encrypt_blobs(store.root()).map_err(|e| format!("加密显形: {e}"))?;
println!(
"encrypt-blobs 完成:新加密 {done} 条,已是密文跳过 {skipped} 条。\n\
库 = {}(段标记 nsblob.<ns>=enc)",
store.db_path().display()
);
Ok(())
}
fn cli_migrate_db() -> Result<(), String> {
let store = ek_cfg::Store::eco().map_err(|e| format!("配置根不可用: {e:?}"))?;
let rep = ek_cfg::migrate_file_to_db(store.root()).map_err(|e| format!("迁移显形: {e}"))?;
println!(
"migrate-db 完成:灌段 {}(跳过 {}),秘值入 blob {},引用缺值 {},中间态改名 {}。\n\
旧档存档 = 根内 *.migrated(回滚 = 去后缀改名还原 + 删 cfg.db)。\
段名:{}",
rep.ns_done.len(),
rep.ns_skipped.len(),
rep.secrets_done,
rep.secrets_missing,
rep.aux_renamed,
if rep.ns_done.is_empty() {
"—".to_string()
} else {
rep.ns_done.join(", ")
}
);
Ok(())
}
fn read_stdin_line() -> Option<String> {
use std::io::BufRead;
let mut s = String::new();
let n = std::io::stdin()
.lock()
.read_line(&mut s)
.ok()
.filter(|n| *n > 0)?;
let _ = n;
Some(s)
}
fn cli_tq_secret(rest: &[String]) -> Result<(), String> {
let dry = rest.iter().any(|a| a == "--check");
use ek_cfg::Store;
use ek_cfg::secret::{self, SecretValue};
use ek_core::section::{Field, SectionMap};
println!("输入天勤 user(回车):");
let user = read_stdin_line()
.ok_or("stdin 缺 user 行")?
.trim()
.to_string();
println!("输入天勤 password(回车,不回显):");
let pass = read_stdin_line()
.ok_or("stdin 缺 password 行")?
.trim_end_matches(['\r', '\n'])
.to_string();
if user.is_empty() || pass.is_empty() {
return Err("user/password 有空行,拒收".to_string());
}
if dry {
for (tag, v) in [("user", user.as_str()), ("password", pass.as_str())] {
let shape: String = v
.chars()
.map(|c| {
if !c.is_ascii() {
'?'
} else if c.is_ascii_alphabetic() {
'A'
} else if c.is_ascii_digit() {
'D'
} else if c == ' ' {
'_'
} else {
'.'
}
})
.collect();
println!("[check] {tag}: 长度={} 每位={shape}", v.chars().count());
}
println!("[check] 未写库(--check 模式)");
return Ok(());
}
if let Some((pos, ch)) = user
.char_indices()
.find(|(_, c)| !c.is_ascii() || c.is_whitespace())
{
return Err(format!(
"用户名第 {} 个字符不是 ASCII(是「{}」,疑似输入法/全角);天勤账号应为字母数字(手机号或账号名),拒收",
user[..pos].chars().count() + 1,
if ch.is_whitespace() {
"空白"
} else {
"非 ASCII 字符"
}
));
}
secret::register(
"tq",
"user",
SecretValue::from_bytes(user.as_bytes().to_vec()),
)
.map_err(|e| format!("密库写入 user 失败:{e}"))?;
secret::register(
"tq",
"password",
SecretValue::from_bytes(pass.as_bytes().to_vec()),
)
.map_err(|e| format!("密库写入 password 失败:{e}"))?;
let store = Store::eco().map_err(|e| format!("档案托底根不可用:{e}"))?;
let map = SectionMap::new(1)
.set("user", Field::secret("user"))
.set("password", Field::secret("password"));
store
.save("tq", &map)
.map_err(|e| format!("tq 段写入失败:{e}"))?;
println!(
"tq 凭据已入密库:user={} password={};存储 = {}(两条引用行)",
secret::get("tq", "user").is_some(),
secret::get("tq", "password").is_some(),
store.db_path().display()
);
Ok(())
}