Skip to content

Commit 6a02b67

Browse files
mastercybclaude
andcommitted
feat(cli): add encode, decode, outboard, keyed-hash, derive-key commands
- encode: file → verified stream (.hemera) - decode: verified stream → file (with root hash verification) - outboard: compute hash tree separately (.obao) - keyed-hash: hash with a 64-byte hex key - derive-key: derive key from context string + file material Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d824a7 commit 6a02b67

1 file changed

Lines changed: 228 additions & 9 deletions

File tree

cli/src/main.rs

Lines changed: 228 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fs::{self, File};
2-
use std::io::{self, Read};
2+
use std::io::{self, Read, Write};
33
use std::path::Path;
44
use std::process;
55

@@ -51,6 +51,60 @@ fn main() {
5151
}
5252
}
5353
}
54+
// hemera encode <file> [-o output] — encode to verified stream
55+
Some("encode") => {
56+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
57+
match rest.as_slice() {
58+
[input] => process::exit(cmd_encode(input, None)),
59+
[input, "-o", output] => process::exit(cmd_encode(input, Some(output))),
60+
_ => {
61+
eprintln!("hemera: encode requires <file> [-o output]");
62+
process::exit(1);
63+
}
64+
}
65+
}
66+
// hemera decode <file> <hash> [-o output] — decode and verify stream
67+
Some("decode") => {
68+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
69+
match rest.as_slice() {
70+
[input, hash] => process::exit(cmd_decode(input, hash, None)),
71+
[input, hash, "-o", output] => process::exit(cmd_decode(input, hash, Some(output))),
72+
_ => {
73+
eprintln!("hemera: decode requires <file> <hash> [-o output]");
74+
process::exit(1);
75+
}
76+
}
77+
}
78+
// hemera outboard <file> [-o output] — compute outboard hash tree
79+
Some("outboard") => {
80+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
81+
match rest.as_slice() {
82+
[input] => process::exit(cmd_outboard(input, None)),
83+
[input, "-o", output] => process::exit(cmd_outboard(input, Some(output))),
84+
_ => {
85+
eprintln!("hemera: outboard requires <file> [-o output]");
86+
process::exit(1);
87+
}
88+
}
89+
}
90+
// hemera keyed-hash <key-hex> <file> — keyed hash
91+
Some("keyed-hash") => {
92+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
93+
if rest.len() != 2 {
94+
eprintln!("hemera: keyed-hash requires <key-hex> <file>");
95+
process::exit(1);
96+
}
97+
process::exit(cmd_keyed_hash(rest[0], rest[1]));
98+
}
99+
// hemera derive-key <context> <file> — derive key from context + material
100+
Some("derive-key") => {
101+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
102+
if rest.len() != 2 {
103+
eprintln!("hemera: derive-key requires <context> <file>");
104+
process::exit(1);
105+
}
106+
process::exit(cmd_derive_key(rest[0], rest[1]));
107+
}
54108
// hemera verify <file> <hash> — verify single file against hash
55109
// hemera verify <checksums> — verify batch from checksum file
56110
Some("verify") => {
@@ -269,6 +323,166 @@ fn verify_checksums(path: &str) -> i32 {
269323
}
270324
}
271325

326+
fn cmd_encode(path: &str, output: Option<&str>) -> i32 {
327+
let data = match fs::read(path) {
328+
Ok(d) => d,
329+
Err(e) => {
330+
eprintln!("hemera: {path}: {e}");
331+
return 1;
332+
}
333+
};
334+
335+
let (root, encoded) = cyber_hemera::stream::encode(&data);
336+
let out_path = output.unwrap_or_else(|| {
337+
// leak is fine — we exit right after
338+
Box::leak(format!("{path}.hemera").into_boxed_str())
339+
});
340+
341+
if let Err(e) = fs::write(out_path, &encoded) {
342+
eprintln!("hemera: {out_path}: {e}");
343+
return 1;
344+
}
345+
346+
println!("{root} {out_path}");
347+
0
348+
}
349+
350+
fn cmd_decode(path: &str, hash_hex: &str, output: Option<&str>) -> i32 {
351+
let encoded = match fs::read(path) {
352+
Ok(d) => d,
353+
Err(e) => {
354+
eprintln!("hemera: {path}: {e}");
355+
return 1;
356+
}
357+
};
358+
359+
let root = match parse_hash(hash_hex) {
360+
Some(h) => h,
361+
None => {
362+
eprintln!("hemera: invalid hash: {hash_hex}");
363+
return 1;
364+
}
365+
};
366+
367+
match cyber_hemera::stream::decode(&encoded, &root) {
368+
Ok(data) => {
369+
if let Some(out_path) = output {
370+
if let Err(e) = fs::write(out_path, &data) {
371+
eprintln!("hemera: {out_path}: {e}");
372+
return 1;
373+
}
374+
println!("{path}: OK → {out_path}");
375+
} else {
376+
let stdout = io::stdout();
377+
let mut handle = stdout.lock();
378+
if let Err(e) = handle.write_all(&data) {
379+
eprintln!("hemera: {e}");
380+
return 1;
381+
}
382+
}
383+
0
384+
}
385+
Err(e) => {
386+
eprintln!("hemera: {path}: {e}");
387+
1
388+
}
389+
}
390+
}
391+
392+
fn cmd_outboard(path: &str, output: Option<&str>) -> i32 {
393+
let data = match fs::read(path) {
394+
Ok(d) => d,
395+
Err(e) => {
396+
eprintln!("hemera: {path}: {e}");
397+
return 1;
398+
}
399+
};
400+
401+
let (root, ob) = cyber_hemera::stream::outboard(&data);
402+
let out_path = output.unwrap_or_else(|| {
403+
Box::leak(format!("{path}.obao").into_boxed_str())
404+
});
405+
406+
if let Err(e) = fs::write(out_path, &ob) {
407+
eprintln!("hemera: {out_path}: {e}");
408+
return 1;
409+
}
410+
411+
println!("{root} {out_path}");
412+
0
413+
}
414+
415+
fn cmd_keyed_hash(key_hex: &str, path: &str) -> i32 {
416+
if key_hex.len() != cyber_hemera::OUTPUT_BYTES * 2 {
417+
eprintln!(
418+
"hemera: key must be {} hex chars ({} bytes)",
419+
cyber_hemera::OUTPUT_BYTES * 2,
420+
cyber_hemera::OUTPUT_BYTES
421+
);
422+
return 1;
423+
}
424+
425+
let key = match hex_to_bytes(key_hex) {
426+
Some(b) => b,
427+
None => {
428+
eprintln!("hemera: invalid hex key: {key_hex}");
429+
return 1;
430+
}
431+
};
432+
433+
let mut key_arr = [0u8; cyber_hemera::OUTPUT_BYTES];
434+
key_arr.copy_from_slice(&key);
435+
436+
let data = match fs::read(path) {
437+
Ok(d) => d,
438+
Err(e) => {
439+
eprintln!("hemera: {path}: {e}");
440+
return 1;
441+
}
442+
};
443+
444+
let h = cyber_hemera::keyed_hash(&key_arr, &data);
445+
println!("{h} {path}");
446+
0
447+
}
448+
449+
fn cmd_derive_key(context: &str, path: &str) -> i32 {
450+
let data = match fs::read(path) {
451+
Ok(d) => d,
452+
Err(e) => {
453+
eprintln!("hemera: {path}: {e}");
454+
return 1;
455+
}
456+
};
457+
458+
let key = cyber_hemera::derive_key(context, &data);
459+
for byte in &key {
460+
print!("{byte:02x}");
461+
}
462+
println!(" {path}");
463+
0
464+
}
465+
466+
fn parse_hash(hex: &str) -> Option<cyber_hemera::Hash> {
467+
let bytes = hex_to_bytes(hex)?;
468+
if bytes.len() != cyber_hemera::OUTPUT_BYTES {
469+
return None;
470+
}
471+
let mut arr = [0u8; cyber_hemera::OUTPUT_BYTES];
472+
arr.copy_from_slice(&bytes);
473+
Some(cyber_hemera::Hash::from_bytes(arr))
474+
}
475+
476+
fn hex_to_bytes(hex: &str) -> Option<Vec<u8>> {
477+
if hex.len() % 2 != 0 {
478+
return None;
479+
}
480+
(0..hex.len())
481+
.step_by(2)
482+
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
483+
.collect()
484+
}
485+
272486
fn print_usage() {
273487
eprintln!(
274488
"\
@@ -285,14 +499,19 @@ fn print_usage() {
285499
t=16 R_F=8 R_P=64 d=7 rate=8 output=64B
286500
genesis: [0x63, 0x79, 0x62, 0x65, 0x72]
287501
\x1b[0m
288-
hemera file1.txt file2.txt Hash files
289-
hemera src/ Hash directory (recursive)
290-
echo hello | hemera Hash stdin
291-
hemera tree file.txt Show tree structure
292-
hemera prove file.txt [chunk] Leaf inclusion proof
293-
hemera prove file.txt 0:4 Subtree inclusion proof
294-
hemera verify file.txt <hash> Verify file against hash
295-
hemera verify sums.txt Verify checksums from file
502+
hemera file1.txt file2.txt Hash files
503+
hemera src/ Hash directory (recursive)
504+
echo hello | hemera Hash stdin
505+
hemera tree file.txt Show tree structure
506+
hemera prove file.txt [chunk] Leaf inclusion proof
507+
hemera prove file.txt 0:4 Subtree inclusion proof
508+
hemera verify file.txt <hash> Verify file against hash
509+
hemera verify sums.txt Verify checksums from file
510+
hemera encode file.txt [-o out] Encode to verified stream
511+
hemera decode file.hemera <hash> Decode and verify stream
512+
hemera outboard file.txt [-o out] Compute outboard hash tree
513+
hemera keyed-hash <key-hex> file Keyed hash
514+
hemera derive-key <context> file Derive key from context
296515
297516
-h, --help Print this help"
298517
);

0 commit comments

Comments
 (0)