use clap::{Parser, Subcommand};
use std::fs;
use std::io::{BufReader, BufWriter, Read as _};
use std::path::Path;
use sha2::{Digest, Sha256};
use tbz_core::envelope::TibetEnvelope;
use tbz_core::manifest::{BlockEntry, Manifest};
use tbz_core::stream::{TbzReader, TbzWriter};
use tbz_core::v2;
use tbz_core::{signature, BlockType};
pub mod tibet_zip;
#[derive(Debug, Clone, Copy, PartialEq)]
enum ArchiveFormat {
TbzBlock,
TibetZip,
Unknown,
}
fn detect_format(path: &str) -> anyhow::Result<ArchiveFormat> {
let mut file = fs::File::open(path)?;
let mut magic = [0u8; 4];
let n = std::io::Read::read(&mut file, &mut magic)?;
if n < 3 {
return Ok(ArchiveFormat::Unknown);
}
if magic[0..3] == [0x54, 0x42, 0x5A] {
Ok(ArchiveFormat::TbzBlock)
} else if magic == [0x50, 0x4B, 0x03, 0x04] {
Ok(ArchiveFormat::TibetZip)
} else {
Ok(ArchiveFormat::Unknown)
}
}
mod mirror_client {
use serde::{Deserialize, Serialize};
const TIMEOUT_SECS: u64 = 5;
#[derive(Serialize)]
pub struct RegisterPayload {
pub content_hash: String,
pub signing_key: String,
pub jis_id: Option<String>,
pub source_repo: Option<String>,
pub block_count: u32,
pub total_size: u64,
}
#[derive(Deserialize)]
pub struct RegisterResponse {
pub status: String, }
#[derive(Deserialize)]
pub struct LookupEntry {
pub content_hash: String,
pub first_seen: String,
pub attestations: Vec<LookupAttestation>,
}
#[derive(Deserialize)]
pub struct LookupAttestation {
pub verdict: String,
}
pub fn register(base_url: &str, payload: &RegisterPayload) -> Result<RegisterResponse, String> {
let url = format!("{}/api/tbz-mirror/register", base_url.trim_end_matches('/'));
let resp = ureq::post(&url)
.timeout(std::time::Duration::from_secs(TIMEOUT_SECS))
.send_json(serde_json::json!({
"content_hash": payload.content_hash,
"signing_key": payload.signing_key,
"jis_id": payload.jis_id,
"source_repo": payload.source_repo,
"block_count": payload.block_count,
"total_size": payload.total_size,
}))
.map_err(|e| e.to_string())?;
resp.into_json::<RegisterResponse>().map_err(|e| e.to_string())
}
pub fn lookup(base_url: &str, hash: &str) -> Result<Option<LookupEntry>, String> {
let url = format!(
"{}/api/tbz-mirror/lookup/{}",
base_url.trim_end_matches('/'),
hash,
);
let resp = ureq::get(&url)
.timeout(std::time::Duration::from_secs(TIMEOUT_SECS))
.call();
match resp {
Ok(r) => {
let entry = r.into_json::<LookupEntry>().map_err(|e| e.to_string())?;
Ok(Some(entry))
}
Err(ureq::Error::Status(404, _)) => Ok(None),
Err(e) => Err(e.to_string()),
}
}
}
fn hash_file(path: &Path) -> anyhow::Result<String> {
let file = fs::File::open(path)?;
let mut reader = BufReader::new(file);
let mut hasher = Sha256::new();
let mut buf = [0u8; 8192];
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(format!("sha256:{:x}", hasher.finalize()))
}
#[derive(Parser)]
#[command(name = "tbz")]
#[command(about = "TBZ (TIBET-zip) — Block-level authenticated compression")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(global = false)]
path: Option<String>,
#[arg(long, global = true, default_value_t = false)]
mirror: bool,
#[arg(long, global = true, env = "TBZ_MIRROR_URL",
default_value = "https://brein.jaspervandemeent.nl")]
mirror_url: String,
}
#[derive(Subcommand)]
enum Commands {
#[command(alias = "p")]
Pack {
path: String,
#[arg(short, long, default_value = "output.tza")]
output: String,
#[arg(long, default_value = "0")]
jis_level: u8,
#[arg(long)]
seal: bool,
#[arg(long, value_name = "PUBKEY_HEX")]
to: Option<String>,
#[arg(long, value_name = "PRIVKEY_PATH")]
from: Option<String>,
#[arg(long = "type", value_name = "CLASS")]
payload_type: Option<String>,
},
#[command(alias = "x")]
Unpack {
archive: String,
#[arg(short, long, default_value = ".")]
output: String,
#[arg(long = "as", value_name = "PRIVKEY_PATH")]
as_key: Option<String>,
#[arg(long)]
no_preview: bool,
#[arg(long)]
strict_type: bool,
},
#[command(alias = "v")]
Verify {
archive: String,
},
#[command(alias = "i")]
Inspect {
archive: String,
},
Init {
#[arg(long, default_value = "github")]
platform: String,
#[arg(long)]
account: Option<String>,
#[arg(long)]
repo: Option<String>,
},
Keygen {
#[arg(short, long, default_value = "tbz-key")]
output: String,
},
}
pub fn run() -> anyhow::Result<()> {
let _ = tracing_subscriber::fmt::try_init();
let cli = Cli::parse();
let mirror_url: Option<&str> = if cli.mirror {
Some(&cli.mirror_url)
} else {
None
};
if let Some(command) = cli.command {
return match command {
Commands::Pack { path, output, jis_level, seal, to, from, payload_type } => {
if seal {
let to_hex = to.ok_or_else(|| anyhow::anyhow!(
"--seal requires --to <pubkey-hex> (64 hex chars)"))?;
let class = match payload_type.as_deref() {
Some(s) => v2::PayloadClass::from_label(s).ok_or_else(|| {
anyhow::anyhow!(
"--type '{}' unknown — use one of: identity, code, document, command, receipt",
s
)
})?,
None => v2::PayloadClass::Unspecified,
};
cmd_pack_sealed(
&path, &output, jis_level, mirror_url,
&to_hex, from.as_deref(), class,
)
} else {
if payload_type.is_some() {
anyhow::bail!("--type only applies to sealed v2 archives (combine with --seal)");
}
cmd_pack(&path, &output, jis_level, mirror_url)
}
}
Commands::Unpack { archive, output, as_key, no_preview, strict_type } => {
cmd_unpack_dispatch(&archive, &output, as_key.as_deref(), !no_preview, strict_type)
}
Commands::Verify { archive } => cmd_verify(&archive, mirror_url),
Commands::Inspect { archive } => cmd_inspect(&archive),
Commands::Init { platform, account, repo } => cmd_init(&platform, account, repo),
Commands::Keygen { output } => cmd_keygen(&output),
};
}
if let Some(path) = cli.path {
let p = Path::new(&path);
let is_tbz_by_magic = if p.is_file() {
match std::fs::File::open(p) {
Ok(mut f) => {
use std::io::Read;
let mut buf = [0u8; 4];
matches!(f.read(&mut buf), Ok(n) if n == 4)
&& buf == [0x54, 0x42, 0x5A, 0x01]
}
Err(_) => false,
}
} else {
false
};
if is_tbz_by_magic {
let extension_matches =
path.ends_with(".tza") || path.ends_with(".tbz");
if !extension_matches {
println!(
"✓ TBZ magic bytes detected — treating as sealed bundle"
);
println!(
" (filename does not carry .tza/.tbz suffix; this may be"
);
println!(
" an operator-renamed bundle. Content is truth, name is hint.)"
);
}
println!("Auto-detected: TBZ envelope → unpack (with airlock verification)\n");
let out_dir = p.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "tbz_out".to_string());
cmd_unpack(&path, &out_dir)?;
return Ok(());
}
if (path.ends_with(".tza") || path.ends_with(".tbz")) && p.is_file() {
anyhow::bail!(
"File '{}' has .tza/.tbz extension but does NOT carry the TBZ magic bytes. \n Refusing to treat as a sealed archive. Use `tbz inspect {}` for details.",
path, path
);
} else if p.is_dir() {
let dir_name = p.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "output".to_string());
let output = format!("{}.tza", dir_name);
println!("Auto-detected: directory → pack to {}\n", output);
cmd_pack(&path, &output, 0, mirror_url)?;
return Ok(());
} else if p.is_file() {
let file_name = p.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "output".to_string());
let output = format!("{}.tza", file_name);
println!("Auto-detected: file → pack to {}\n", output);
cmd_pack(&path, &output, 0, mirror_url)?;
return Ok(());
} else {
anyhow::bail!("Path not found: {}", path);
}
}
Cli::parse_from(["tbz", "--help"]);
Ok(())
}
fn cmd_pack(path: &str, output: &str, default_jis_level: u8, mirror_url: Option<&str>) -> anyhow::Result<()> {
let source = Path::new(path);
if !source.exists() {
anyhow::bail!("Source path does not exist: {}", path);
}
let files = collect_files(source)?;
println!("TBZ pack: {} file(s) from {}", files.len(), path);
let jis_manifest = tbz_jis::JisManifest::load(Path::new(".")).ok();
if let Some(ref jis) = jis_manifest {
println!(" .jis.json found: {}", jis.repo_identifier());
}
let (signing_key, verifying_key) = signature::generate_keypair();
let mut manifest = Manifest::new();
for (i, (file_path, data)) in files.iter().enumerate() {
let jis_level = jis_manifest
.as_ref()
.map(|j| j.jis_level_for_path(file_path))
.unwrap_or(default_jis_level);
manifest.add_block(BlockEntry {
index: (i + 1) as u32,
block_type: "data".to_string(),
compressed_size: 0, uncompressed_size: data.len() as u64,
jis_level,
description: file_path.clone(),
path: Some(file_path.clone()),
});
}
manifest.set_signing_key(&verifying_key);
let out_file = fs::File::create(output)?;
let mut writer = TbzWriter::new(BufWriter::new(out_file), signing_key);
writer.write_manifest(&manifest)?;
println!(" [0] manifest ({} block entries)", manifest.blocks.len());
for (file_path, data) in &files {
let jis_level = jis_manifest
.as_ref()
.map(|j| j.jis_level_for_path(file_path))
.unwrap_or(default_jis_level);
let envelope = TibetEnvelope::new(
signature::sha256_hash(data),
"data",
mime_for_path(file_path),
"tbz-cli",
&format!("Pack file: {}", file_path),
vec!["block:0".to_string()],
);
let envelope = if let Some(ref jis) = jis_manifest {
envelope.with_source_repo(&jis.repo_identifier())
} else {
envelope
};
writer.write_data_block(data, jis_level, &envelope)?;
println!(
" [{}] {} ({} bytes, JIS level {})",
writer.block_count() - 1,
file_path,
data.len(),
jis_level,
);
}
let total_blocks = writer.block_count();
writer.finish();
let vk_hex = hex_encode(&verifying_key.to_bytes());
println!("\nArchive written: {}", output);
println!(" Blocks: {}", total_blocks);
println!(" Signing key (Ed25519 public): {}", vk_hex);
println!(" Format: TBZ v{}", tbz_core::VERSION);
if let Some(url) = mirror_url {
let archive_hash = hash_file(Path::new(output))?;
println!("\n Mirror: registering {} ...", archive_hash);
let jis_id = jis_manifest.as_ref().map(|_| {
format!("jis:ed25519:{}", &vk_hex[..16])
});
let source_repo = jis_manifest.as_ref().map(|j| j.repo_identifier());
let payload = mirror_client::RegisterPayload {
content_hash: archive_hash,
signing_key: vk_hex.clone(),
jis_id,
source_repo,
block_count: total_blocks as u32,
total_size: fs::metadata(output).map(|m| m.len()).unwrap_or(0),
};
match mirror_client::register(url, &payload) {
Ok(resp) => println!(" Mirror: {} ({})", resp.status, url),
Err(e) => println!(" Mirror: WARNING — {}", e),
}
}
Ok(())
}
fn cmd_inspect(archive: &str) -> anyhow::Result<()> {
match detect_format(archive)? {
ArchiveFormat::TibetZip => return tibet_zip::inspect(archive),
ArchiveFormat::Unknown => anyhow::bail!("Not a TBZ archive: unrecognized format"),
ArchiveFormat::TbzBlock => {} }
let file = fs::File::open(archive)?;
let mut reader = TbzReader::new(std::io::BufReader::new(file));
println!("TBZ inspect: {}\n", archive);
println!(" Magic: 0x54425A (TBZ)");
println!(" Format: v{}\n", tbz_core::VERSION);
let mut block_idx = 0;
while let Some(block) = reader.read_block()? {
let type_str = match block.header.block_type {
BlockType::Manifest => "MANIFEST",
BlockType::Data => "DATA",
BlockType::Nested => "NESTED",
};
println!(" Block {} [{}]", block.header.block_index, type_str);
println!(" JIS level: {}", block.header.jis_level);
println!(" Compressed: {} bytes", block.header.compressed_size);
println!(" Uncompressed: {} bytes", block.header.uncompressed_size);
println!(" TIBET ERIN hash: {}", block.envelope.erin.content_hash);
println!(" TIBET ERACHTER: {}", block.envelope.erachter);
if let Some(ref repo) = block.envelope.eromheen.source_repo {
println!(" Source repo: {}", repo);
}
if block.header.block_type == BlockType::Manifest {
if let Ok(decompressed) = block.decompress() {
if let Ok(manifest) = serde_json::from_slice::<Manifest>(&decompressed) {
println!(" --- Manifest ---");
println!(" Total blocks: {}", manifest.block_count);
println!(" Total uncompressed: {} bytes", manifest.total_uncompressed_size);
println!(" Max JIS level: {}", manifest.max_jis_level());
for entry in &manifest.blocks {
println!(
" [{:>3}] {} — {} bytes, JIS {}",
entry.index,
entry.path.as_deref().unwrap_or(&entry.description),
entry.uncompressed_size,
entry.jis_level,
);
}
}
}
}
let sig_nonzero = block.signature.iter().any(|&b| b != 0);
println!(" Signature: {}", if sig_nonzero { "Ed25519 (present)" } else { "none" });
println!();
block_idx += 1;
}
println!(" Total: {} blocks", block_idx);
Ok(())
}
fn cmd_unpack(archive: &str, output_dir: &str) -> anyhow::Result<()> {
match detect_format(archive)? {
ArchiveFormat::TibetZip => return tibet_zip::unpack(archive, output_dir),
ArchiveFormat::Unknown => anyhow::bail!("Not a TBZ archive: unrecognized format"),
ArchiveFormat::TbzBlock => {} }
println!("TBZ unpack: {} → {}\n", archive, output_dir);
println!(" Airlock pre-check: verifying archive integrity...\n");
{
let vfile = fs::File::open(archive)?;
let mut vreader = TbzReader::new(std::io::BufReader::new(vfile));
let mut errors = 0u32;
let mut block_count = 0u32;
let mut verifying_key: Option<tbz_core::VerifyingKey> = None;
while let Some(block) = vreader.read_block()? {
if let Err(_) = block.validate() {
errors += 1;
block_count += 1;
continue;
}
if block.header.block_type == BlockType::Manifest {
if let Ok(decompressed) = block.decompress() {
if let Ok(manifest) = serde_json::from_slice::<Manifest>(&decompressed) {
verifying_key = manifest.get_verifying_key();
}
}
}
if let Some(ref vk) = verifying_key {
if block.verify_signature(vk).is_err() {
errors += 1;
}
}
match block.decompress() {
Ok(decompressed) => {
let actual_hash = signature::sha256_hash(&decompressed);
if actual_hash != block.envelope.erin.content_hash {
errors += 1;
}
}
Err(_) => { errors += 1; }
}
block_count += 1;
}
if errors > 0 {
anyhow::bail!(
"AIRLOCK BREACH BLOCKED — archive corrupt: {} ({} block errors in {} blocks). \
Use `tbz verify` to inspect, or fix the archive.",
archive, errors, block_count
);
}
println!(" Airlock pre-check: {} blocks verified ✓\n", block_count);
}
let file = fs::File::open(archive)?;
let mut reader = TbzReader::new(std::io::BufReader::new(file));
let mut airlock = tbz_airlock::Airlock::new(256 * 1024 * 1024, 30);
println!(" Airlock mode: {:?}\n", airlock.mode());
fs::create_dir_all(output_dir)?;
let mut block_idx = 0;
let mut manifest: Option<Manifest> = None;
while let Some(block) = reader.read_block()? {
match block.header.block_type {
BlockType::Manifest => {
let decompressed = block.decompress()?;
manifest = Some(serde_json::from_slice(&decompressed)
.map_err(|e| anyhow::anyhow!("Invalid manifest: {}", e))?);
println!(" [0] Manifest parsed ({} entries)", manifest.as_ref().unwrap().blocks.len());
}
BlockType::Data => {
let decompressed = block.decompress()?;
airlock.allocate(decompressed.len() as u64)?;
airlock.receive(&decompressed)?;
let file_path = manifest
.as_ref()
.and_then(|m| {
m.blocks.iter()
.find(|e| e.index == block.header.block_index)
.and_then(|e| e.path.clone())
})
.unwrap_or_else(|| format!("block_{}", block.header.block_index));
let out_path = Path::new(output_dir).join(&file_path);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let data = airlock.release(); fs::write(&out_path, &data)?;
println!(
" [{}] {} ({} bytes) ✓",
block.header.block_index,
file_path,
data.len(),
);
}
BlockType::Nested => {
println!(" [{}] Nested TBZ (not yet supported)", block.header.block_index);
}
}
block_idx += 1;
}
println!("\n Extracted {} blocks via Airlock", block_idx);
println!(" Airlock buffer: wiped (0x00)");
Ok(())
}
fn cmd_verify(archive: &str, mirror_url: Option<&str>) -> anyhow::Result<()> {
match detect_format(archive)? {
ArchiveFormat::TibetZip => return tibet_zip::verify(archive),
ArchiveFormat::Unknown => anyhow::bail!("Not a TBZ archive: unrecognized format"),
ArchiveFormat::TbzBlock => {} }
let file = fs::File::open(archive)?;
let mut reader = TbzReader::new(std::io::BufReader::new(file));
println!("TBZ verify: {}\n", archive);
let mut errors = 0;
let mut block_idx = 0;
let mut verifying_key: Option<tbz_core::VerifyingKey> = None;
while let Some(block) = reader.read_block()? {
if let Err(e) = block.validate() {
println!(" [{}] FAIL header: {}", block.header.block_index, e);
errors += 1;
block_idx += 1;
continue;
}
if block.header.block_type == BlockType::Manifest {
if let Ok(decompressed) = block.decompress() {
if let Ok(manifest) = serde_json::from_slice::<Manifest>(&decompressed) {
verifying_key = manifest.get_verifying_key();
if let Some(ref vk) = verifying_key {
let vk_hex = hex_encode(&vk.to_bytes());
println!(" Signing key: Ed25519 {}", &vk_hex[..16]);
println!();
} else {
println!(" WARNING: No signing key in manifest — signature checks skipped\n");
}
}
}
}
let sig_ok = if let Some(ref vk) = verifying_key {
match block.verify_signature(vk) {
Ok(()) => true,
Err(e) => {
println!(" [{}] FAIL signature: {}", block.header.block_index, e);
errors += 1;
false
}
}
} else {
true };
match block.decompress() {
Ok(decompressed) => {
let actual_hash = signature::sha256_hash(&decompressed);
if actual_hash == block.envelope.erin.content_hash {
let sig_status = if verifying_key.is_some() && sig_ok {
"hash + signature"
} else if verifying_key.is_some() {
"hash only (sig FAILED)"
} else {
"hash only (no key)"
};
println!(" [{}] OK — {} verified", block.header.block_index, sig_status);
} else {
println!(
" [{}] FAIL — hash mismatch\n expected: {}\n actual: {}",
block.header.block_index,
block.envelope.erin.content_hash,
actual_hash,
);
errors += 1;
}
}
Err(e) => {
println!(" [{}] FAIL — decompress error: {}", block.header.block_index, e);
errors += 1;
}
}
block_idx += 1;
}
println!();
if errors == 0 {
if verifying_key.is_some() {
println!(" Result: ALL {} BLOCKS VERIFIED (hash + Ed25519) ✓", block_idx);
} else {
println!(" Result: ALL {} BLOCKS VERIFIED (hash only, no signing key) ✓", block_idx);
}
} else {
println!(" Result: {} ERRORS in {} blocks ✗", errors, block_idx);
}
if let Some(url) = mirror_url {
let archive_hash = hash_file(Path::new(archive))?;
match mirror_client::lookup(url, &archive_hash) {
Ok(Some(entry)) => {
let verdicts: Vec<&str> = entry.attestations.iter()
.map(|a| a.verdict.as_str())
.collect();
println!("\n Mirror: KNOWN");
println!(" Hash: {}", entry.content_hash);
println!(" First seen: {}", entry.first_seen);
println!(
" Attestations: {} ({})",
entry.attestations.len(),
if verdicts.is_empty() { "none".to_string() } else { verdicts.join(", ") },
);
}
Ok(None) => {
println!("\n Mirror: UNKNOWN — not registered in Transparency Mirror");
}
Err(e) => {
println!("\n Mirror: WARNING — {}", e);
}
}
}
Ok(())
}
fn cmd_init(platform: &str, account: Option<String>, repo: Option<String>) -> anyhow::Result<()> {
let account = account.unwrap_or_else(|| "<your-account>".to_string());
let repo = repo.unwrap_or_else(|| {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
.unwrap_or_else(|| "<repo>".to_string())
});
let tbz_dir = Path::new(".tbz");
let key_path = tbz_dir.join("signing.key");
let pub_path = tbz_dir.join("signing.pub");
let (signing_key, verifying_key) = if key_path.exists() {
let sk_hex = fs::read_to_string(&key_path)?;
let sk_bytes: Vec<u8> = (0..sk_hex.trim().len())
.step_by(2)
.filter_map(|i| u8::from_str_radix(&sk_hex.trim()[i..i + 2], 16).ok())
.collect();
if sk_bytes.len() != 32 {
anyhow::bail!("Invalid signing key in .tbz/signing.key");
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&sk_bytes);
let sk = tbz_core::SigningKey::from_bytes(&key_array);
let vk = sk.verifying_key();
println!("Using existing keypair from .tbz/");
(sk, vk)
} else {
let (sk, vk) = signature::generate_keypair();
fs::create_dir_all(tbz_dir)?;
fs::write(&key_path, hex_encode(&sk.to_bytes()))?;
fs::write(&pub_path, hex_encode(&vk.to_bytes()))?;
println!("Generated Ed25519 keypair:");
println!(" Private: .tbz/signing.key (KEEP SECRET — add to .gitignore!)");
println!(" Public: .tbz/signing.pub");
(sk, vk)
};
let vk_hex = hex_encode(&verifying_key.to_bytes());
let jis_id = format!("jis:ed25519:{}", &vk_hex[..16]);
let claim_data = format!("{}:{}:{}:{}", platform, account, repo, vk_hex);
let claim_sig = signature::sign(claim_data.as_bytes(), &signing_key);
let jis_json = serde_json::json!({
"tbz": "1.0",
"jis_id": jis_id,
"signing_key": vk_hex,
"claim": {
"platform": platform,
"account": account,
"repo": repo,
"intent": "official_releases",
"sectors": {
"src/*": { "jis_level": 0, "description": "Public source code" },
"keys/*": { "jis_level": 2, "description": "Signing keys" }
}
},
"tibet": {
"erin": "Repository identity binding",
"eraan": [&jis_id],
"erachter": format!("Provenance root for TBZ packages from {}/{}", account, repo)
},
"signature": hex_encode(&claim_sig),
"timestamp": chrono_now()
});
let output = serde_json::to_string_pretty(&jis_json)?;
fs::write(".jis.json", &output)?;
let gitignore = Path::new(".gitignore");
if gitignore.exists() {
let content = fs::read_to_string(gitignore)?;
if !content.contains(".tbz/signing.key") {
fs::write(gitignore, format!("{}\n# TBZ signing key (NEVER commit!)\n.tbz/signing.key\n", content.trim_end()))?;
println!("\n Added .tbz/signing.key to .gitignore");
}
}
println!("\nGenerated .jis.json:");
println!(" JIS ID: {}", jis_id);
println!(" Claim: {}/{}/{}", platform, account, repo);
println!(" Signature: Ed25519 (signed)");
Ok(())
}
fn collect_files(path: &Path) -> anyhow::Result<Vec<(String, Vec<u8>)>> {
let mut files = Vec::new();
if path.is_file() {
let data = fs::read(path)?;
let name = path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "file".to_string());
files.push((name, data));
} else if path.is_dir() {
collect_dir_recursive(path, path, &mut files)?;
}
Ok(files)
}
fn collect_dir_recursive(
base: &Path,
current: &Path,
files: &mut Vec<(String, Vec<u8>)>,
) -> anyhow::Result<()> {
let mut entries: Vec<_> = fs::read_dir(current)?.collect::<Result<_, _>>()?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') || name == "target" || name == "node_modules" {
continue;
}
if path.is_file() {
let rel = path.strip_prefix(base)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| name);
let data = fs::read(&path)?;
files.push((rel, data));
} else if path.is_dir() {
collect_dir_recursive(base, &path, files)?;
}
}
Ok(())
}
fn mime_for_path(path: &str) -> &str {
match path.rsplit('.').next() {
Some("rs") => "text/x-rust",
Some("toml") => "application/toml",
Some("json") => "application/json",
Some("md") => "text/markdown",
Some("txt") => "text/plain",
Some("py") => "text/x-python",
Some("js") => "text/javascript",
Some("html") => "text/html",
Some("css") => "text/css",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("bin") => "application/octet-stream",
_ => "application/octet-stream",
}
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn hex_decode_32(s: &str) -> anyhow::Result<[u8; 32]> {
let s = s.trim();
if s.len() != 64 {
anyhow::bail!("expected 64 hex characters, got {}", s.len());
}
let mut out = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let pair = std::str::from_utf8(chunk).map_err(|_| anyhow::anyhow!("invalid utf8"))?;
out[i] = u8::from_str_radix(pair, 16)
.map_err(|e| anyhow::anyhow!("invalid hex pair '{}': {}", pair, e))?;
}
Ok(out)
}
fn read_signing_key_from_file(path: &str) -> anyhow::Result<ed25519_dalek::SigningKey> {
let raw = fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("cannot read key file {}: {}", path, e))?;
let bytes = hex_decode_32(&raw)?;
Ok(ed25519_dalek::SigningKey::from_bytes(&bytes))
}
fn chrono_now() -> String {
use std::time::SystemTime;
let duration = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
format!("{}Z", duration.as_secs())
}
fn cmd_keygen(output: &str) -> anyhow::Result<()> {
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let priv_path = format!("{}.priv", output);
let pub_path = format!("{}.pub", output);
let priv_hex = hex_encode(&signing_key.to_bytes());
fs::write(&priv_path, &priv_hex)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o600);
fs::set_permissions(&priv_path, perms)?;
}
let pub_hex = hex_encode(&verifying_key.to_bytes());
fs::write(&pub_path, &pub_hex)?;
println!("TBZ keygen: Ed25519 keypair generated\n");
println!(" Private: {} (mode 0600)", priv_path);
println!(" Public: {}", pub_path);
println!("\n Pubkey (share this): {}", pub_hex);
println!("\n Use with:");
println!(" tibet-zip pack <dir> -o sealed.tza --seal --to {} --from {}", pub_hex, priv_path);
println!(" tibet-zip unpack sealed.tza -o out/ --as {}", priv_path);
Ok(())
}
fn cmd_pack_sealed(
path: &str,
output: &str,
default_jis_level: u8,
mirror_url: Option<&str>,
receiver_hex: &str,
sender_priv_path: Option<&str>,
payload_class: v2::PayloadClass,
) -> anyhow::Result<()> {
let source = Path::new(path);
if !source.exists() {
anyhow::bail!("Source path does not exist: {}", path);
}
let receiver_pubkey = hex_decode_32(receiver_hex)
.map_err(|e| anyhow::anyhow!("--to pubkey: {}", e))?;
let sender_key = match sender_priv_path {
Some(p) => read_signing_key_from_file(p)?,
None => {
use rand::rngs::OsRng;
ed25519_dalek::SigningKey::generate(&mut OsRng)
}
};
println!("TBZ pack (sealed v2): {} → {}", path, output);
println!(" Sender pubkey: {}", hex_encode(&sender_key.verifying_key().to_bytes()));
println!(" Receiver pubkey: {}", receiver_hex);
println!(" Payload class: {}", payload_class.label());
let v1_bytes = build_v1_archive_bytes(source, default_jis_level, mirror_url)?;
println!("\n Inner v1 archive: {} bytes", v1_bytes.len());
let container = v2::write_sealed_container_with_class(
&sender_key,
&receiver_pubkey,
&v1_bytes,
payload_class,
)
.map_err(|e| anyhow::anyhow!("v2 seal failed: {}", e))?;
fs::write(output, &container)?;
println!(" Outer v2 envelope: {} bytes (overhead: {} bytes)", container.len(), container.len() - v1_bytes.len());
println!("\n Sealed: {} ✓", output);
println!(" Format: TBZ v2 (AES-256-GCM, Ed25519-signed)");
Ok(())
}
fn build_v1_archive_bytes(
source: &Path,
default_jis_level: u8,
_mirror_url: Option<&str>,
) -> anyhow::Result<Vec<u8>> {
let files = collect_files(source)?;
let jis_manifest = tbz_jis::JisManifest::load(Path::new(".")).ok();
let (signing_key, verifying_key) = signature::generate_keypair();
let mut manifest = Manifest::new();
for (i, (file_path, data)) in files.iter().enumerate() {
let jis_level = jis_manifest
.as_ref()
.map(|j| j.jis_level_for_path(file_path))
.unwrap_or(default_jis_level);
manifest.add_block(BlockEntry {
index: (i + 1) as u32,
block_type: "data".to_string(),
compressed_size: 0,
uncompressed_size: data.len() as u64,
jis_level,
description: file_path.clone(),
path: Some(file_path.clone()),
});
}
manifest.set_signing_key(&verifying_key);
let mut buf: Vec<u8> = Vec::new();
{
let mut writer = TbzWriter::new(&mut buf, signing_key);
writer.write_manifest(&manifest)?;
for (file_path, data) in &files {
let jis_level = jis_manifest
.as_ref()
.map(|j| j.jis_level_for_path(file_path))
.unwrap_or(default_jis_level);
let envelope = TibetEnvelope::new(
signature::sha256_hash(data),
"data",
mime_for_path(file_path),
"tbz-cli",
&format!("Pack file: {}", file_path),
vec!["block:0".to_string()],
);
let envelope = if let Some(ref jis) = jis_manifest {
envelope.with_source_repo(&jis.repo_identifier())
} else {
envelope
};
writer.write_data_block(data, jis_level, &envelope)?;
}
writer.finish();
}
Ok(buf)
}
fn cmd_unpack_dispatch(
archive: &str,
output_dir: &str,
as_key: Option<&str>,
preview: bool,
strict_type: bool,
) -> anyhow::Result<()> {
let mut head = [0u8; 32];
let n = {
let mut f = fs::File::open(archive)?;
std::io::Read::read(&mut f, &mut head)?
};
let version = if n >= 7 { v2::detect_version(&head[..n]) } else { 0 };
if version == 2 {
let priv_path = as_key.ok_or_else(|| anyhow::anyhow!(
"{} is a v2 sealed archive — pass --as <privkey-path> to decrypt", archive))?;
let receiver_key = read_signing_key_from_file(priv_path)?;
println!("TBZ unpack (v2 sealed): {} → {}", archive, output_dir);
let container = fs::read(archive)?;
let result = v2::read_sealed_container_full(&container, &receiver_key);
let (env, plain, payload_class) = match result {
Ok(t) => t,
Err(e) => {
let (sender_hex, receiver_hex, uuid_hex) = peek_v2_envelope_metadata(&container);
emit_unseal_audit(
&sender_hex,
&receiver_hex,
&uuid_hex,
"unknown",
0,
&format!("error:{:?}", e).replace('"', "'"),
);
return Err(anyhow::anyhow!("v2 unseal failed: {}", e));
}
};
println!(" Sender: {}", hex_encode(&env.sender_pubkey));
println!(" Receiver: {} ✓", hex_encode(&env.receiver_pubkey));
println!(" Declared payload class: {}", payload_class.label());
println!(" Inner v1 archive: {} bytes\n", plain.len());
match check_class_vs_extension(archive, payload_class) {
ClassCheck::Ok => {}
ClassCheck::Warn(msg) | ClassCheck::Mismatch(msg) => {
if strict_type {
anyhow::bail!("payload-class/extension mismatch (strict mode): {}", msg);
} else {
println!(" ⚠ payload-class hint: {}", msg);
println!(" (use --strict-type to make this fatal)\n");
}
}
}
if preview {
preview_inner_manifest(&plain, payload_class, strict_type)?;
}
let tmp = std::env::temp_dir().join(format!("tbz-v2-inner-{}.tza", std::process::id()));
fs::write(&tmp, &plain)?;
let result = cmd_unpack(tmp.to_str().unwrap(), output_dir);
let _ = fs::remove_file(&tmp);
let outcome = if result.is_ok() { "success" } else { "extract-failed" };
emit_unseal_audit(
&hex_encode(&env.sender_pubkey),
&hex_encode(&env.receiver_pubkey),
&hex_encode(&env.archive_uuid),
payload_class.label(),
plain.len(),
outcome,
);
result
} else {
cmd_unpack(archive, output_dir)
}
}
fn peek_v2_envelope_metadata(container: &[u8]) -> (String, String, String) {
let min = 3 + 4 + 32 + 32 + 16;
if container.len() < min { return (String::new(), String::new(), String::new()); }
let mut off = 3 + 4;
let sender = &container[off..off + 32]; off += 32;
let receiver = &container[off..off + 32]; off += 32;
let uuid = &container[off..off + 16];
(hex_encode(sender), hex_encode(receiver), hex_encode(uuid))
}
enum ClassCheck {
Ok,
Warn(String),
Mismatch(String),
}
fn check_class_vs_extension(archive: &str, class: v2::PayloadClass) -> ClassCheck {
let ext = Path::new(archive)
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
use v2::PayloadClass::*;
let hint = match (class, ext.as_deref()) {
(Unspecified, _) => return ClassCheck::Ok,
(Identity, Some("aint")) | (Identity, Some("id")) | (Identity, Some("tza")) => return ClassCheck::Ok,
(Code, Some("tza")) | (Code, Some("bin")) | (Code, Some("exec")) => return ClassCheck::Ok,
(Document, Some("tza")) | (Document, Some("doc")) | (Document, Some("pdf"))
| (Document, Some("txt")) | (Document, Some("md")) => return ClassCheck::Ok,
(Command, Some("tza")) | (Command, Some("cmd")) | (Command, Some("req")) => return ClassCheck::Ok,
(Receipt, Some("tza")) | (Receipt, Some("ack")) | (Receipt, Some("rcpt")) => return ClassCheck::Ok,
(Identity, Some(e)) => format!("outer .{} but declared class = identity", e),
(Code, Some(e)) => format!("outer .{} but declared class = code", e),
(Document, Some(e)) => format!("outer .{} but declared class = document", e),
(Command, Some(e)) => format!("outer .{} but declared class = command", e),
(Receipt, Some(e)) => format!("outer .{} but declared class = receipt", e),
(_, None) => format!("no extension on outer file but declared class = {}", class.label()),
};
ClassCheck::Mismatch(hint)
}
fn emit_unseal_audit(
sender_pubkey_hex: &str,
receiver_pubkey_hex: &str,
archive_uuid_hex: &str,
payload_class: &str,
payload_size: usize,
outcome: &str,
) {
use std::io::Write as _;
let timestamp = chrono_now();
let event_id = format!("tbz-unseal-{}-{}", std::process::id(), timestamp);
let record = format!(
"{{\"event\":\"tbz-unseal.v1\",\"event_id\":\"{}\",\"timestamp\":\"{}\",\"sender_pubkey\":\"{}\",\"receiver_pubkey\":\"{}\",\"archive_uuid\":\"{}\",\"payload_class\":\"{}\",\"payload_size\":{},\"outcome\":\"{}\",\"_emitter\":\"tibet-zip-cli@{}\"}}\n",
event_id,
timestamp,
sender_pubkey_hex,
receiver_pubkey_hex,
archive_uuid_hex,
payload_class,
payload_size,
outcome,
env!("CARGO_PKG_VERSION"),
);
let candidates: Vec<std::path::PathBuf> = {
let mut v = Vec::new();
if let Ok(p) = std::env::var("TBZ_UNSEAL_AUDIT_LOG") {
v.push(std::path::PathBuf::from(p));
}
v.push(std::path::PathBuf::from("/var/log/tibet/tbz-unseal.jsonl"));
let xdg = std::env::var("XDG_STATE_HOME")
.ok()
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| format!("{}/.local/state", h))
});
if let Some(base) = xdg {
v.push(std::path::PathBuf::from(format!("{}/tbz/audit.jsonl", base)));
}
v
};
for path in candidates {
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(&path) {
if f.write_all(record.as_bytes()).is_ok() {
return; }
}
}
}
fn preview_inner_manifest(
inner: &[u8],
class: v2::PayloadClass,
strict_type: bool,
) -> anyhow::Result<()> {
use tbz_core::stream::TbzReader;
let mut reader = TbzReader::new(std::io::Cursor::new(inner));
let mut shown = 0u32;
let mut warn_files: Vec<String> = Vec::new();
println!(" Preview (= no disk write yet):");
while let Some(block) = reader.read_block()? {
if block.header.block_type == BlockType::Manifest {
if let Ok(json) = block.decompress() {
if let Ok(manifest) = serde_json::from_slice::<Manifest>(&json) {
for entry in &manifest.blocks {
let path = entry.path.as_deref().unwrap_or(&entry.description);
println!(
" [{:>3}] {:<40} {:>10} bytes JIS {}",
entry.index, path, entry.uncompressed_size, entry.jis_level
);
shown += 1;
if class != v2::PayloadClass::Code {
let lower = path.to_ascii_lowercase();
let exec = lower.ends_with(".exe")
|| lower.ends_with(".bat")
|| lower.ends_with(".cmd")
|| lower.ends_with(".sh")
|| lower.ends_with(".ps1")
|| lower.ends_with(".vbs");
if exec {
warn_files.push(path.to_string());
}
}
}
}
}
break; }
}
if shown == 0 {
println!(" (no manifest entries found)");
}
if !warn_files.is_empty() {
let msg = format!(
"executable file(s) found inside a non-code envelope: {}",
warn_files.join(", ")
);
if strict_type {
anyhow::bail!("strict-type: {}", msg);
} else {
println!("\n ⚠ {}", msg);
println!(" (declared class = {} — pass --strict-type to refuse)\n",
class.label());
}
} else {
println!();
}
Ok(())
}