use clap::{Parser, Subcommand};
use colored::*;
use std::fs;
use std::path::{Path, PathBuf};
use vsf::decoding::parse::parse;
use vsf::file_format::VsfHeader;
use vsf::inspect::{
format_bytes, format_eagle_time, format_number, format_value, format_value_short,
labels_from_header, parse_section_fields, LabelInfo,
};
use vsf::types::VsfType;
#[derive(Parser)]
#[command(name = "vsfinfo")]
#[command(about = "VSF File Inspector - Inspect, verify, and extract VSF file contents", long_about = None)]
#[command(version)]
struct Cli {
#[arg(value_name = "FILE")]
file: Option<PathBuf>,
#[arg(short, long)]
detailed: bool,
#[arg(short, long)]
key: Option<PathBuf>,
#[arg(long, value_name = "HEX")]
symmetric_key: Option<String>,
#[arg(long, value_name = "HEX")]
identity_seed: Option<String>,
#[arg(long, value_name = "HEX")]
their_seed: Option<String>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Info,
Verify,
#[command(name = "get")]
Extract {
#[arg(value_name = "FIELD_PATH")]
field_path: String,
},
Tree,
#[command(name = "seed")]
DeriveSeed {
#[arg(value_name = "HANDLE")]
handle: String,
},
}
fn main() {
let cli = Cli::parse();
if let Some(Commands::DeriveSeed { handle }) = cli.command {
if let Err(e) = derive_seed(&handle) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
return;
}
let file = match cli.file {
Some(f) => f,
None => {
use clap::CommandFactory;
Cli::command().print_help().unwrap();
println!();
std::process::exit(0);
}
};
let raw_data = match fs::read(&file) {
Ok(d) => d,
Err(e) => {
eprintln!("Error reading file: {}", e);
std::process::exit(1);
}
};
let data = if let (Some(ref our_seed_hex), Some(ref their_seed_hex)) =
(&cli.identity_seed, &cli.their_seed)
{
match decrypt_photon_contact_state(&raw_data, our_seed_hex, their_seed_hex) {
Ok(d) => d,
Err(e) => {
eprintln!("Decryption error: {}", e);
std::process::exit(1);
}
}
} else if let Some(ref seed_hex) = cli.identity_seed {
match decrypt_photon_contacts(&raw_data, seed_hex) {
Ok(d) => d,
Err(e) => {
eprintln!("Decryption error: {}", e);
std::process::exit(1);
}
}
} else if let Some(ref key_hex) = cli.symmetric_key {
match decrypt_symmetric(&raw_data, key_hex) {
Ok(d) => d,
Err(e) => {
eprintln!("Decryption error: {}", e);
std::process::exit(1);
}
}
} else {
raw_data
};
let result = match cli.command {
Some(Commands::Info) | None => show_info(&data, cli.detailed, cli.key.as_deref()),
Some(Commands::Verify) => verify_file(&data),
Some(Commands::Extract { field_path }) => extract_field(&data, &field_path),
Some(Commands::Tree) => show_tree(&data),
Some(Commands::DeriveSeed { handle }) => derive_seed(&handle),
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
#[cfg(feature = "crypto")]
fn decrypt_photon_contacts(encrypted: &[u8], seed_hex: &str) -> Result<Vec<u8>, String> {
use blake3::Hasher;
let seed = hex::decode(seed_hex).map_err(|e| format!("Invalid hex: {}", e))?;
if seed.len() != 32 {
return Err(format!("Seed must be 32 bytes (got {})", seed.len()));
}
let mut hasher = Hasher::new();
hasher.update(b"photon_contact_list_v2");
hasher.update(&seed);
let key = hasher.finalize();
decrypt_chacha20poly1305(encrypted, key.as_bytes())
}
#[cfg(not(feature = "crypto"))]
fn decrypt_photon_contacts(_encrypted: &[u8], _seed_hex: &str) -> Result<Vec<u8>, String> {
Err("Crypto feature not enabled - rebuild with --features crypto".to_string())
}
#[cfg(feature = "crypto")]
fn decrypt_photon_contact_state(
encrypted: &[u8],
our_seed_hex: &str,
their_seed_hex: &str,
) -> Result<Vec<u8>, String> {
use blake3::Hasher;
let our_seed = hex::decode(our_seed_hex).map_err(|e| format!("Invalid our_seed hex: {}", e))?;
let their_seed =
hex::decode(their_seed_hex).map_err(|e| format!("Invalid their_seed hex: {}", e))?;
if our_seed.len() != 32 {
return Err(format!(
"Our seed must be 32 bytes (got {})",
our_seed.len()
));
}
if their_seed.len() != 32 {
return Err(format!(
"Their seed must be 32 bytes (got {})",
their_seed.len()
));
}
let mut hasher = Hasher::new();
hasher.update(b"photon_contact_state_v2");
hasher.update(&our_seed);
hasher.update(&their_seed);
let key = hasher.finalize();
decrypt_chacha20poly1305(encrypted, key.as_bytes())
}
#[cfg(not(feature = "crypto"))]
fn decrypt_photon_contact_state(
_encrypted: &[u8],
_our_seed_hex: &str,
_their_seed_hex: &str,
) -> Result<Vec<u8>, String> {
Err("Crypto feature not enabled - rebuild with --features crypto".to_string())
}
#[cfg(feature = "crypto")]
fn decrypt_symmetric(encrypted: &[u8], key_hex: &str) -> Result<Vec<u8>, String> {
let key = hex::decode(key_hex).map_err(|e| format!("Invalid hex: {}", e))?;
if key.len() != 32 {
return Err(format!("Key must be 32 bytes (got {})", key.len()));
}
let key_array: [u8; 32] = key.try_into().unwrap();
decrypt_chacha20poly1305(encrypted, &key_array)
}
#[cfg(not(feature = "crypto"))]
fn decrypt_symmetric(_encrypted: &[u8], _key_hex: &str) -> Result<Vec<u8>, String> {
Err("Crypto feature not enabled - rebuild with --features crypto".to_string())
}
#[cfg(feature = "crypto")]
fn decrypt_chacha20poly1305(encrypted: &[u8], key: &[u8; 32]) -> Result<Vec<u8>, String> {
use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, KeyInit, Nonce};
if encrypted.len() < 12 + 16 {
return Err("File too short for ChaCha20-Poly1305 (need at least 28 bytes)".to_string());
}
let cipher =
ChaCha20Poly1305::new_from_slice(key).map_err(|e| format!("Key init error: {}", e))?;
let nonce_bytes: [u8; 12] = encrypted[..12]
.try_into()
.map_err(|_| "Invalid nonce length")?;
let nonce: Nonce = nonce_bytes.into();
let ciphertext = &encrypted[12..];
cipher
.decrypt(&nonce, ciphertext)
.map_err(|e| format!("Decryption failed: {} (wrong key?)", e))
}
fn show_info(data: &[u8], _detailed: bool, _key_path: Option<&Path>) -> Result<(), String> {
let formatted = vsf::inspect::inspect_vsf(data)?;
println!("{}", formatted);
Ok(())
}
fn verify_integrity_summary(
data: &[u8],
header: &VsfHeader,
labels: &[LabelInfo],
) -> Result<bool, String> {
let mut all_checks_pass = true;
if let VsfType::hp(ref stored_hash) = header.provenance_hash {
println!(
" {}-Byte {} {}:",
stored_hash.len().to_string().white(),
"BLAKE3".green(),
"provenance hash".cyan()
);
print!(" {} ", "0x".truecolor(64, 50, 255));
for byte in stored_hash.iter() {
print!("{:02X}", byte);
}
println!();
}
if let Some(VsfType::ke(ref pk_bytes)) = header.signer_pubkey {
println!(
" {}-Byte {} {}:",
pk_bytes.len().to_string().white(),
"Ed25519".green(),
"signer pubkey".cyan()
);
print!(" {} ", "0x".truecolor(64, 50, 255));
for byte in pk_bytes.iter() {
print!("{:02X}", byte);
}
println!();
}
if let Some(VsfType::ge(ref sig_bytes)) = header.signature {
println!(
" {}-Byte {} {}:",
sig_bytes.len().to_string().white(),
"Ed25519".green(),
"signature".cyan()
);
print!(" {} ", "0x".truecolor(64, 50, 255));
for byte in sig_bytes.iter().take(32) {
print!("{:02X}", byte);
}
if sig_bytes.len() > 32 {
print!("...");
}
println!();
println!(
" {} {}",
"Semantics:".cyan(),
"Protocol-specific (signed data unknown)".truecolor(200, 200, 200)
);
}
let (file_hash_verified, stored_hash, computed_hash) =
if let Some(VsfType::hb(ref stored_hash)) = header.rolling_hash {
let computed = vsf::verification::compute_file_hash(data).unwrap_or_else(|_| [0u8; 32]);
let verified = computed.as_slice() == stored_hash.as_slice();
(verified, Some(stored_hash.clone()), Some(computed.to_vec()))
} else {
(false, None, None)
};
let mut verified_sections = 0;
let mut total_sections = 0;
for label in labels {
if label.child_count > 0 {
total_sections += 1;
if let Some(ref hash_vsf) = label.hash {
let hash_bytes = match hash_vsf {
VsfType::hp(ref bytes) | VsfType::hb(ref bytes) | VsfType::hs(ref bytes) => {
bytes
}
_ => continue,
};
let section_end = label.offset + label.size;
if section_end <= data.len() {
let section_data = &data[label.offset..section_end];
let computed = blake3::hash(section_data);
if computed.as_bytes() == hash_bytes.as_slice() {
verified_sections += 1;
}
}
}
}
}
let _ = (verified_sections, total_sections);
if stored_hash.is_some() {
println!(
" {}-Byte {} {}:",
32.to_string().white(),
"BLAKE3".green(),
"rolling hash".cyan()
);
}
if file_hash_verified {
if let Some(hash) = stored_hash {
print!(" {} ", "0x".truecolor(64, 50, 255));
for byte in hash.iter() {
print!("{:02X}", byte);
}
println!();
}
print!(" {} ", "Verification:".cyan());
println!("{}", "PASS".truecolor(0, 255, 0));
} else if stored_hash.is_some() {
all_checks_pass = false;
if let (Some(expected), Some(computed)) = (stored_hash, computed_hash) {
print!(" {} {} ", "Expected:".cyan(), "0x".truecolor(64, 50, 255));
for byte in expected.iter() {
print!("{:02X}", byte);
}
println!();
print!(" {} {} ", "Got:".cyan(), " 0x".truecolor(64, 50, 255));
for byte in computed.iter() {
print!("{:02X}", byte);
}
println!();
}
print!(" {} ", "Verification:".cyan());
println!("{}", "FAIL".truecolor(255, 0, 0));
}
Ok(all_checks_pass)
}
fn verify_file(data: &[u8]) -> Result<(), String> {
println!("Verifying VSF file...\n");
let mut errors = 0;
let mut warnings = 0;
if data.len() < 4 || &data[0..3] != "RÅ".as_bytes() || data[3] != b'<' {
println!("✗ Invalid magic number");
errors += 1;
} else {
println!("✓ Magic number valid");
}
let (header, _) = match VsfHeader::decode(data) {
Ok(h) => {
println!("✓ Header structure valid");
h
}
Err(e) => {
println!("✗ Header parsing failed: {}", e);
errors += 1;
return Err("Cannot continue verification".into());
}
};
let labels = labels_from_header(&header);
for label in &labels {
if let Some(ref hash_vsf) = label.hash {
let hash_bytes = match hash_vsf {
VsfType::hb(ref bytes) | VsfType::hs(ref bytes) => bytes,
_ => continue,
};
let section_end = label.offset + label.size;
if section_end <= data.len() {
let section_data = &data[label.offset..section_end];
let computed = blake3::hash(section_data);
if computed.as_bytes() == hash_bytes.as_slice() {
println!("✓ Section '{}': hash verified", label.name);
} else {
println!("✗ Section '{}': hash mismatch!", label.name);
errors += 1;
}
} else {
println!("✗ Section '{}': section exceeds file size", label.name);
errors += 1;
}
}
if label.signature.is_some() {
println!(
"✓ Section '{}': signature present (verification TBD)",
label.name
);
warnings += 1;
}
}
if labels
.iter()
.any(|l| l.name == "token_auth" || l.name == "token auth")
{
println!("\n✓ Found TOKEN auth section");
println!(" (Full signature verification TBD)");
warnings += 1;
} else {
println!("\n○ No TOKEN auth section found");
}
println!("\n{}", "=".repeat(50));
if errors == 0 && warnings == 0 {
println!("✓ ALL CHECKS PASSED");
} else if errors == 0 {
println!("✓ VALID ({} warnings)", warnings);
} else {
println!("✗ INVALID ({} errors, {} warnings)", errors, warnings);
}
Ok(())
}
fn extract_field(data: &[u8], field_path: &str) -> Result<(), String> {
let parts: Vec<&str> = field_path.split('.').collect();
if parts.len() != 2 {
return Err("Field path must be 'section.field'".into());
}
let section_name = parts[0];
let field_name = parts[1];
let (header, _) = VsfHeader::decode(data)?;
let labels = labels_from_header(&header);
let section = labels
.iter()
.find(|l| {
l.name == section_name
|| l.name.replace(' ', "_") == section_name
|| l.name.replace('_', " ") == section_name
})
.ok_or(format!("Section '{}' not found", section_name))?;
let fields = parse_section_fields(data, section)?;
for field in fields {
if field.name == field_name
|| field.name.replace(' ', "_") == field_name
|| field.name.replace('_', " ") == field_name
{
let values_str: Vec<String> = field.values.iter().map(|v| format_value(v)).collect();
println!("{}", values_str.join(", "));
return Ok(());
}
}
Err(format!(
"Field '{}' not found in section '{}'",
field_name, section_name
))
}
fn show_tree(data: &[u8]) -> Result<(), String> {
let (header, _) = VsfHeader::decode(data)?;
let labels = labels_from_header(&header);
println!("VSF File Tree");
println!("{}", "=".repeat(50));
println!();
for (i, label) in labels.iter().enumerate() {
let is_last = i == labels.len() - 1;
let prefix = if is_last { "└── " } else { "├── " };
println!(
"{}{} ({} Bytes, {} fields)",
prefix, label.name, label.size, label.child_count
);
if let Ok(fields) = parse_section_fields(data, label) {
for (j, field) in fields.iter().enumerate() {
let is_field_last = j == fields.len() - 1;
let field_prefix = if is_last { " " } else { "│ " };
let field_marker = if is_field_last {
"└── "
} else {
"├── "
};
println!("{}{}{}: ", field_prefix, field_marker, field.name);
let continuation_prefix = if is_field_last { " " } else { "│ " };
let mut line_buffer = String::new();
for val_vsf in field.values.iter() {
let val_str = format_value_short(val_vsf);
let is_opcode = matches!(val_vsf, VsfType::op(_, _));
if is_opcode && !line_buffer.is_empty() {
println!("{}{}", field_prefix, line_buffer);
line_buffer.clear();
}
if line_buffer.is_empty() {
line_buffer.push_str(&format!("{} ", continuation_prefix));
}
line_buffer.push_str(&val_str);
}
if !line_buffer.is_empty() {
println!("{}{}", field_prefix, line_buffer);
}
}
}
if !is_last {
println!("│");
}
}
Ok(())
}
fn derive_seed(handle: &str) -> Result<(), String> {
use blake3::Hasher;
let vsf_bytes = VsfType::x(handle.to_string()).flatten();
let hash = blake3::hash(&vsf_bytes);
let seed = hash.as_bytes();
let mut hasher = Hasher::new();
hasher.update(b"photon_contact_list_v2");
hasher.update(seed);
let list_key = hasher.finalize();
println!("{}", "Photon Identity Seed Derivation".cyan().bold());
println!();
println!(" {} {}", "Handle:".cyan(), handle.white());
println!(
" {} {} Bytes",
"VSF encoding:".cyan(),
vsf_bytes.len().to_string().white()
);
println!();
println!(
" {} {}",
"Identity seed:".cyan(),
hex::encode(seed).to_uppercase().yellow()
);
println!(
" {} {}",
"Directory:".cyan(),
hex::encode(&seed[..8]).white()
);
println!();
println!(
" {} {}",
"Contact list key:".cyan(),
hex::encode(list_key.as_bytes()).to_uppercase().green()
);
println!();
println!("{}", "Usage:".cyan().bold());
println!(
" vsfinfo --symmetric-key {} index.enc",
hex::encode(list_key.as_bytes()).to_uppercase()
);
Ok(())
}