chore: bump to 0.7.1, workflow/readme/init/upgrade updates, fix clippy needless_borrows
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m47s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 48s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m2s
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m47s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 48s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 1m2s
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Made-with: Cursor
This commit is contained in:
@@ -5,6 +5,8 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'src/**'
|
- 'src/**'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -331,11 +333,14 @@ jobs:
|
|||||||
- name: 安装依赖
|
- name: 安装依赖
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
|
$cargoBin = Join-Path $env:USERPROFILE ".cargo\bin"
|
||||||
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
|
||||||
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe
|
Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile rustup-init.exe
|
||||||
.\rustup-init.exe -y --default-toolchain stable
|
.\rustup-init.exe -y --default-toolchain stable
|
||||||
Remove-Item rustup-init.exe
|
Remove-Item rustup-init.exe
|
||||||
}
|
}
|
||||||
|
$env:Path = "$cargoBin;$env:Path"
|
||||||
|
Add-Content -Path $env:GITHUB_PATH -Value $cargoBin
|
||||||
rustup target add x86_64-pc-windows-msvc
|
rustup target add x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1836,7 +1836,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ cargo build --release
|
|||||||
# 1. 配置数据库连接(会先验证连接可用再写入)
|
# 1. 配置数据库连接(会先验证连接可用再写入)
|
||||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets"
|
||||||
|
|
||||||
# 2. 初始化主密钥(提示输入主密码,派生后存入 OS 钥匙串)
|
# 2. 初始化主密钥(提示输入至少 8 位的主密码,派生后存入 OS 钥匙串)
|
||||||
secrets init
|
secrets init
|
||||||
```
|
```
|
||||||
|
|
||||||
主密码不会存储,仅用于派生主密钥。同一主密码在所有设备上会得到相同主密钥(salt 存于数据库,首台设备生成后共享)。
|
主密码不会存储,仅用于派生主密钥,且至少需 8 位。同一主密码在所有设备上会得到相同主密钥(salt 存于数据库,首台设备生成后共享)。
|
||||||
|
|
||||||
**主密钥存储**:macOS → Keychain;Windows → Credential Manager;Linux → keyutils(会话级,重启后需再次 `secrets init`)。
|
**主密钥存储**:macOS → Keychain;Windows → Credential Manager;Linux → keyutils(会话级,重启后需再次 `secrets init`)。
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ secrets update -n refining --kind service --name mqtt --remove-meta old_port --r
|
|||||||
secrets delete -n refining --kind service --name legacy-mqtt
|
secrets delete -n refining --kind service --name legacy-mqtt
|
||||||
|
|
||||||
# ── init ─────────────────────────────────────────────────────────────────────
|
# ── init ─────────────────────────────────────────────────────────────────────
|
||||||
secrets init # 主密钥初始化(每台设备一次,主密码派生后存钥匙串)
|
secrets init # 主密钥初始化(每台设备一次,主密码至少 8 位,派生后存钥匙串)
|
||||||
|
|
||||||
# ── config ───────────────────────────────────────────────────────────────────
|
# ── config ───────────────────────────────────────────────────────────────────
|
||||||
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets" # 先验证再写入
|
secrets config set-db "postgres://postgres:<password>@<host>:<port>/secrets" # 先验证再写入
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ use sqlx::PgPool;
|
|||||||
|
|
||||||
use crate::{crypto, db};
|
use crate::{crypto, db};
|
||||||
|
|
||||||
|
const MIN_MASTER_PASSWORD_LEN: usize = 8;
|
||||||
|
|
||||||
pub async fn run(pool: &PgPool) -> Result<()> {
|
pub async fn run(pool: &PgPool) -> Result<()> {
|
||||||
println!("Initializing secrets master key...");
|
println!("Initializing secrets master key...");
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Read password (no echo)
|
// Read password (no echo)
|
||||||
let password =
|
let password = rpassword::prompt_password(format!(
|
||||||
rpassword::prompt_password("Enter master password: ").context("failed to read password")?;
|
"Enter master password (at least {} characters): ",
|
||||||
if password.is_empty() {
|
MIN_MASTER_PASSWORD_LEN
|
||||||
anyhow::bail!("Master password must not be empty.");
|
))
|
||||||
|
.context("failed to read password")?;
|
||||||
|
if password.chars().count() < MIN_MASTER_PASSWORD_LEN {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Master password must be at least {} characters.",
|
||||||
|
MIN_MASTER_PASSWORD_LEN
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let confirm = rpassword::prompt_password("Confirm master password: ")
|
let confirm = rpassword::prompt_password("Confirm master password: ")
|
||||||
.context("failed to read password confirmation")?;
|
.context("failed to read password confirmation")?;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use flate2::read::GzDecoder;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const GITEA_API: &str = "https://gitea.refining.dev/api/v1/repos/refining/secrets/releases/latest";
|
const GITEA_API: &str = "https://gitea.refining.dev/api/v1/repos/refining/secrets/releases/latest";
|
||||||
|
|
||||||
@@ -28,16 +29,17 @@ fn available_assets(assets: &[Asset]) -> String {
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_asset_by_suffix<'a>(assets: &'a [Asset], suffix: &str) -> Result<&'a Asset> {
|
fn release_asset_name(tag_name: &str, suffix: &str) -> String {
|
||||||
assets
|
format!("secrets-{tag_name}-{suffix}")
|
||||||
.iter()
|
}
|
||||||
.find(|a| a.name.ends_with(suffix))
|
|
||||||
.with_context(|| {
|
fn find_asset_by_name<'a>(assets: &'a [Asset], name: &str) -> Result<&'a Asset> {
|
||||||
format!(
|
assets.iter().find(|a| a.name == name).with_context(|| {
|
||||||
"no asset found for this platform (looking for suffix: {suffix})\navailable: {}",
|
format!(
|
||||||
available_assets(assets)
|
"no matching release asset found: {name}\navailable: {}",
|
||||||
)
|
available_assets(assets)
|
||||||
})
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect the asset suffix for the current platform/arch at compile time.
|
/// Detect the asset suffix for the current platform/arch at compile time.
|
||||||
@@ -89,6 +91,22 @@ fn sha256_hex(bytes: &[u8]) -> String {
|
|||||||
format!("{digest:x}")
|
format!("{digest:x}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_checksum(asset_name: &str, archive: &[u8], checksum_contents: &str) -> Result<String> {
|
||||||
|
let expected_checksum = parse_checksum_file(checksum_contents)?;
|
||||||
|
let actual_checksum = sha256_hex(archive);
|
||||||
|
|
||||||
|
if actual_checksum != expected_checksum {
|
||||||
|
bail!(
|
||||||
|
"checksum verification failed for {}: expected {}, got {}",
|
||||||
|
asset_name,
|
||||||
|
expected_checksum,
|
||||||
|
actual_checksum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(actual_checksum)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_checksum_file(contents: &str) -> Result<String> {
|
fn parse_checksum_file(contents: &str) -> Result<String> {
|
||||||
let checksum = contents
|
let checksum = contents
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
@@ -163,6 +181,8 @@ pub async fn run(check_only: bool) -> Result<()> {
|
|||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.user_agent(format!("secrets-cli/{CURRENT_VERSION}"))
|
.user_agent(format!("secrets-cli/{CURRENT_VERSION}"))
|
||||||
|
.connect_timeout(Duration::from_secs(10))
|
||||||
|
.timeout(Duration::from_secs(120))
|
||||||
.build()
|
.build()
|
||||||
.context("failed to build HTTP client")?;
|
.context("failed to build HTTP client")?;
|
||||||
|
|
||||||
@@ -192,18 +212,10 @@ pub async fn run(check_only: bool) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let suffix = platform_asset_suffix()?;
|
let suffix = platform_asset_suffix()?;
|
||||||
let asset = find_asset_by_suffix(&release.assets, suffix)?;
|
let asset_name = release_asset_name(&release.tag_name, suffix);
|
||||||
|
let asset = find_asset_by_name(&release.assets, &asset_name)?;
|
||||||
let checksum_name = format!("{}.sha256", asset.name);
|
let checksum_name = format!("{}.sha256", asset.name);
|
||||||
let checksum_asset = release
|
let checksum_asset = find_asset_by_name(&release.assets, &checksum_name)?;
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|a| a.name == checksum_name)
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"missing checksum asset for download: {checksum_name}\navailable: {}",
|
|
||||||
available_assets(&release.assets)
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!("Downloading {}...", asset.name);
|
println!("Downloading {}...", asset.name);
|
||||||
|
|
||||||
@@ -214,19 +226,11 @@ pub async fn run(check_only: bool) -> Result<()> {
|
|||||||
"checksum download",
|
"checksum download",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let expected_checksum = parse_checksum_file(
|
let actual_checksum = verify_checksum(
|
||||||
|
&asset.name,
|
||||||
|
&archive,
|
||||||
std::str::from_utf8(&checksum_contents).context("checksum file is not valid UTF-8")?,
|
std::str::from_utf8(&checksum_contents).context("checksum file is not valid UTF-8")?,
|
||||||
)?;
|
)?;
|
||||||
let actual_checksum = sha256_hex(&archive);
|
|
||||||
|
|
||||||
if actual_checksum != expected_checksum {
|
|
||||||
bail!(
|
|
||||||
"checksum verification failed for {}: expected {}, got {}",
|
|
||||||
asset.name,
|
|
||||||
expected_checksum,
|
|
||||||
actual_checksum
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Verified SHA-256: {actual_checksum}");
|
println!("Verified SHA-256: {actual_checksum}");
|
||||||
|
|
||||||
@@ -298,6 +302,33 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("invalid SHA-256 checksum format"));
|
assert!(err.to_string().contains("invalid SHA-256 checksum format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_asset_name_matches_release_tag() {
|
||||||
|
assert_eq!(
|
||||||
|
release_asset_name("secrets-0.7.0", "x86_64-linux-musl.tar.gz"),
|
||||||
|
"secrets-secrets-0.7.0-x86_64-linux-musl.tar.gz"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_asset_by_name_rejects_stale_platform_match() {
|
||||||
|
let assets = vec![
|
||||||
|
Asset {
|
||||||
|
name: "secrets-secrets-0.6.9-x86_64-linux-musl.tar.gz".into(),
|
||||||
|
browser_download_url: "https://example.invalid/old".into(),
|
||||||
|
},
|
||||||
|
Asset {
|
||||||
|
name: "secrets-secrets-0.7.0-aarch64-macos.tar.gz".into(),
|
||||||
|
browser_download_url: "https://example.invalid/other".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let err = find_asset_by_name(&assets, "secrets-secrets-0.7.0-x86_64-linux-musl.tar.gz")
|
||||||
|
.expect_err("stale asset should not match");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("no matching release asset found"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sha256_hex_matches_known_value() {
|
fn sha256_hex_matches_known_value() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -306,6 +337,18 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_checksum_rejects_mismatch() {
|
||||||
|
let err = verify_checksum(
|
||||||
|
"secrets.tar.gz",
|
||||||
|
b"abc",
|
||||||
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef secrets.tar.gz",
|
||||||
|
)
|
||||||
|
.expect_err("checksum mismatch should fail");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("checksum verification failed"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extract_from_targz_reads_binary() {
|
fn extract_from_targz_reads_binary() {
|
||||||
let payload = b"fake-secrets-binary";
|
let payload = b"fake-secrets-binary";
|
||||||
|
|||||||
Reference in New Issue
Block a user