cpop_protocol/war/
encoding.rs1use super::types::{Block, Seal, Version};
4use chrono::{DateTime, Utc};
5use hex;
6
7impl Block {
8 pub fn encode_ascii(&self) -> String {
10 let mut output = String::new();
11
12 output.push_str("-----BEGIN CPOP WAR-----\n");
13 output.push_str(&format!("Version: {}\n", self.version.as_str()));
14 output.push_str(&format!("Author: {}\n", self.author));
15 output.push_str(&format!("Document-ID: {}\n", hex::encode(self.document_id)));
16 output.push_str(&format!("Timestamp: {}\n", self.timestamp.to_rfc3339()));
17 if let Some(nonce) = &self.verifier_nonce {
18 output.push_str(&format!("Verifier-Nonce: {}\n", hex::encode(nonce)));
19 }
20 output.push('\n');
21
22 for line in word_wrap(&self.statement, 72) {
23 output.push_str(&line);
24 output.push('\n');
25 }
26
27 output.push('\n');
28 output.push_str("-----BEGIN SEAL-----\n");
29
30 let seal_hex = self.seal.encode_hex();
31 for chunk in seal_hex.as_bytes().chunks(64) {
32 output.push_str(std::str::from_utf8(chunk).unwrap_or(""));
33 output.push('\n');
34 }
35
36 output.push_str("-----END SEAL-----\n");
37 output.push_str("-----END CPOP WAR-----\n");
38
39 output
40 }
41
42 pub fn decode_ascii(text: &str) -> Result<Self, String> {
44 let lines: Vec<&str> = text.lines().collect();
45
46 let start = lines
48 .iter()
49 .position(|l| {
50 l.contains("BEGIN CPOP WAR")
51 || l.contains("BEGIN POP WAR")
52 || l.contains("BEGIN WITNESSD AUTHORSHIP RECORD")
53 })
54 .ok_or("missing WAR block header")?;
55 let end = lines
56 .iter()
57 .position(|l| {
58 l.contains("END CPOP WAR")
59 || l.contains("END POP WAR")
60 || l.contains("END WITNESSD AUTHORSHIP RECORD")
61 })
62 .ok_or("missing WAR block footer")?;
63
64 if start >= end {
65 return Err("invalid block structure".to_string());
66 }
67
68 let mut version = Version::V1_0;
69 let mut author = String::new();
70 let mut document_id = [0u8; 32];
71 let mut timestamp = Utc::now();
72 let mut verifier_nonce: Option<[u8; 32]> = None;
73 let mut header_end = start + 1;
74
75 for (i, line) in lines[start + 1..end].iter().enumerate() {
76 if line.is_empty() {
77 header_end = start + 1 + i;
78 break;
79 }
80
81 if let Some(val) = line.strip_prefix("Version: ") {
82 version =
83 Version::parse(val.trim()).ok_or_else(|| format!("unknown version: {val}"))?;
84 } else if let Some(val) = line.strip_prefix("Author: ") {
85 author = val.trim().to_string();
86 } else if let Some(val) = line.strip_prefix("Document-ID: ") {
87 let bytes =
88 hex::decode(val.trim()).map_err(|e| format!("invalid document ID: {e}"))?;
89 if bytes.len() != 32 {
90 return Err("document ID must be 32 bytes".to_string());
91 }
92 document_id.copy_from_slice(&bytes);
93 } else if let Some(val) = line.strip_prefix("Timestamp: ") {
94 timestamp = DateTime::parse_from_rfc3339(val.trim())
95 .map_err(|e| format!("invalid timestamp: {e}"))?
96 .with_timezone(&Utc);
97 } else if let Some(val) = line.strip_prefix("Verifier-Nonce: ") {
98 let bytes =
99 hex::decode(val.trim()).map_err(|e| format!("invalid verifier nonce: {e}"))?;
100 if bytes.len() != 32 {
101 return Err("verifier nonce must be 32 bytes".to_string());
102 }
103 let mut nonce = [0u8; 32];
104 nonce.copy_from_slice(&bytes);
105 verifier_nonce = Some(nonce);
106 }
107 }
108
109 let seal_start = lines[start..end]
110 .iter()
111 .position(|l| l.contains("BEGIN SEAL"))
112 .map(|pos| start + pos)
113 .ok_or("missing seal header")?;
114 let seal_end = lines[start..end]
115 .iter()
116 .position(|l| l.contains("END SEAL"))
117 .map(|pos| start + pos)
118 .ok_or("missing seal footer")?;
119
120 let statement_lines: Vec<&str> = lines[header_end + 1..seal_start]
121 .iter()
122 .filter(|l| !l.is_empty())
123 .copied()
124 .collect();
125 let statement = statement_lines.join(" ");
126
127 let seal_hex: String = lines[seal_start + 1..seal_end]
128 .iter()
129 .map(|l| l.trim())
130 .collect();
131 let seal = Seal::decode_hex(&seal_hex)?;
132 let signed = seal.signature != [0u8; 64];
133
134 Ok(Self {
135 version,
136 author,
137 document_id,
138 timestamp,
139 statement,
140 seal,
141 signed,
142 verifier_nonce,
143 ear: None,
144 })
145 }
146}
147
148impl Seal {
149 pub fn encode_hex(&self) -> String {
151 let mut data = Vec::with_capacity(32 * 3 + 64 + 32);
152 data.extend_from_slice(&self.h1);
153 data.extend_from_slice(&self.h2);
154 data.extend_from_slice(&self.h3);
155 data.extend_from_slice(&self.signature);
156 data.extend_from_slice(&self.public_key);
157 hex::encode(data)
158 }
159
160 pub fn decode_hex(hex_str: &str) -> Result<Self, String> {
162 let data = hex::decode(hex_str).map_err(|e| format!("invalid seal hex: {e}"))?;
163 if data.len() != 32 * 3 + 64 + 32 {
164 return Err(format!(
165 "invalid seal length: expected {}, got {}",
166 32 * 3 + 64 + 32,
167 data.len()
168 ));
169 }
170
171 let mut h1 = [0u8; 32];
172 let mut h2 = [0u8; 32];
173 let mut h3 = [0u8; 32];
174 let mut signature = [0u8; 64];
175 let mut public_key = [0u8; 32];
176
177 h1.copy_from_slice(&data[0..32]);
178 h2.copy_from_slice(&data[32..64]);
179 h3.copy_from_slice(&data[64..96]);
180 signature.copy_from_slice(&data[96..160]);
181 public_key.copy_from_slice(&data[160..192]);
182
183 Ok(Self {
184 h1,
185 h2,
186 h3,
187 signature,
188 public_key,
189 })
190 }
191}
192
193pub fn word_wrap(text: &str, width: usize) -> Vec<String> {
195 let mut lines = Vec::new();
196 let mut current_line = String::new();
197
198 for word in text.split_whitespace() {
199 if current_line.is_empty() {
200 current_line = word.to_string();
201 } else if current_line.len() + 1 + word.len() <= width {
202 current_line.push(' ');
203 current_line.push_str(word);
204 } else {
205 lines.push(current_line);
206 current_line = word.to_string();
207 }
208 }
209
210 if !current_line.is_empty() {
211 lines.push(current_line);
212 }
213
214 lines
215}