1pub mod manifest;
10pub mod payment;
11#[cfg(any(all(target_arch = "wasm32", feature = "browser-wasm"), test))]
12mod peer_records;
13pub mod protocol;
14
15pub use manifest::{
16 parse_browser_manifest, validate_browser_payment_network, BrowserManifest,
17 BrowserManifestEndpoint, PublicFileDescriptor, BROWSER_MANIFEST_VERSION,
18};
19pub use payment::{storage_payment_total, verify_storage_quote, VerifiedStorageQuote};
20pub use protocol::{
21 parse_webrtc_direct_multiaddr, BrowserPaymentNetwork, BrowserQuoteArtifact,
22 WebRtcDirectEndpoint, BROWSER_PROTOCOL_NAME, BROWSER_PROTOCOL_VERSION,
23 WEBRTC_DIRECT_DATA_CHANNEL,
24};
25
26#[cfg(all(target_arch = "wasm32", feature = "browser-wasm"))]
27mod wasm_transport;
28
29use bytes::Bytes;
30use self_encryption::{DataMap, EncryptedChunk};
31use serde::{Deserialize, Serialize};
32use std::collections::HashSet;
33
34pub const MAX_BROWSER_FILE_BYTES: usize = 1_000_000_000;
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct BrowserChunkInfo {
43 pub index: usize,
45 pub dst_hash: String,
47 pub src_hash: String,
49 pub src_size: usize,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct BrowserRecord {
56 pub address: String,
58 #[serde(with = "serde_bytes")]
60 pub content: Vec<u8>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct BrowserRecordInfo {
66 pub address: String,
68 pub size: usize,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct BrowserStagedFile {
75 pub name: String,
77 pub content_type: String,
79 pub address: String,
81 #[serde(default)]
83 pub blake3: String,
84 #[serde(default)]
86 pub size: usize,
87 #[serde(default)]
89 pub data_map_size: usize,
90 #[serde(default)]
92 pub chunks: Vec<BrowserChunkInfo>,
93 pub records: Vec<BrowserRecordInfo>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct BrowserEncryptedFile {
100 pub address: String,
102 pub blake3: String,
104 pub data_map_size: usize,
106 pub chunks: Vec<BrowserChunkInfo>,
108 pub records: Vec<BrowserRecord>,
110}
111
112#[derive(Debug, thiserror::Error)]
114pub enum BrowserError {
115 #[error("invalid browser data: {0}")]
117 Invalid(String),
118 #[error("self-encryption failed: {0}")]
120 SelfEncryption(String),
121 #[error("DataMap serialization failed: {0}")]
123 DataMap(String),
124}
125
126#[must_use]
129pub fn content_address(content: &[u8]) -> String {
130 hex::encode(ant_protocol::compute_address(content))
131}
132
133pub fn verify_record(address: &str, content: &[u8]) -> Result<(), BrowserError> {
136 let expected = address.strip_prefix("0x").unwrap_or(address);
137 if expected.len() != 64 || !expected.bytes().all(|byte| byte.is_ascii_hexdigit()) {
138 return Err(BrowserError::Invalid(
139 "record address must be 32 hexadecimal bytes".to_string(),
140 ));
141 }
142 let mut address = [0; 32];
143 hex::decode_to_slice(expected, &mut address)
144 .map_err(|e| BrowserError::Invalid(e.to_string()))?;
145 crate::record::verify(&address, content).map_err(BrowserError::Invalid)
146}
147
148pub fn encrypt_public_file(content: &[u8]) -> Result<BrowserEncryptedFile, BrowserError> {
151 if content.len() < self_encryption::MIN_ENCRYPTABLE_BYTES {
152 return Err(BrowserError::Invalid(format!(
153 "self-encryption requires at least {} bytes",
154 self_encryption::MIN_ENCRYPTABLE_BYTES
155 )));
156 }
157 if content.len() > MAX_BROWSER_FILE_BYTES {
158 return Err(BrowserError::Invalid(format!(
159 "browser files are limited to {MAX_BROWSER_FILE_BYTES} bytes"
160 )));
161 }
162
163 let whole_file_hash = blake3::hash(content).to_hex().to_string();
164 let (published_data_map, encrypted_chunks) =
165 self_encryption::encrypt(Bytes::copy_from_slice(content))
166 .map_err(|error| BrowserError::SelfEncryption(error.to_string()))?;
167 let root_data_map = {
168 let encrypted_by_address = encrypted_chunks
169 .iter()
170 .map(|chunk| {
171 (
172 ant_protocol::compute_address(&chunk.content),
173 &chunk.content,
174 )
175 })
176 .collect::<std::collections::HashMap<_, _>>();
177 let mut get_local_chunk = |address: self_encryption::XorName| {
178 encrypted_by_address
179 .get(&address.0)
180 .map(|content| (*content).clone())
181 .ok_or_else(|| {
182 self_encryption::Error::Generic(format!(
183 "self-encryption output omitted DataMap chunk {}",
184 hex::encode(address.0)
185 ))
186 })
187 };
188 self_encryption::get_root_data_map(published_data_map.clone(), &mut get_local_chunk)
189 .map_err(|error| BrowserError::SelfEncryption(error.to_string()))?
190 };
191 let chunks = chunk_infos(&root_data_map);
192
193 let mut records: Vec<BrowserRecord> = encrypted_chunks
194 .into_iter()
195 .map(|chunk| BrowserRecord {
196 address: content_address(&chunk.content),
197 content: chunk.content.to_vec(),
198 })
199 .collect();
200 let (address, encoded_data_map) =
201 crate::client_engine::files::public_map_record(&published_data_map)
202 .map_err(BrowserError::DataMap)?;
203 let address = hex::encode(address);
204 let data_map_size = encoded_data_map.len();
205 records.push(BrowserRecord {
206 address: address.clone(),
207 content: encoded_data_map.to_vec(),
208 });
209
210 Ok(BrowserEncryptedFile {
211 address,
212 blake3: whole_file_hash,
213 data_map_size,
214 chunks,
215 records,
216 })
217}
218
219pub fn decode_public_data_map(content: &[u8]) -> Result<Vec<BrowserChunkInfo>, BrowserError> {
221 let data_map: DataMap =
222 rmp_serde::from_slice(content).map_err(|error| BrowserError::DataMap(error.to_string()))?;
223 Ok(chunk_infos(&data_map))
224}
225
226pub fn decrypt_public_file(
229 data_map_content: &[u8],
230 encrypted_contents: &[Vec<u8>],
231) -> Result<Vec<u8>, BrowserError> {
232 let data_map: DataMap = rmp_serde::from_slice(data_map_content)
233 .map_err(|error| BrowserError::DataMap(error.to_string()))?;
234 let available = encrypted_contents
235 .iter()
236 .map(|content| ant_protocol::compute_address(content))
237 .collect::<HashSet<_>>();
238 for info in data_map.infos() {
239 if !available.contains(&info.dst_hash.0) {
240 return Err(BrowserError::Invalid(format!(
241 "record set does not contain DataMap chunk {}; a record may be missing or corrupt",
242 hex::encode(info.dst_hash.0)
243 )));
244 }
245 }
246 let encrypted_chunks = encrypted_contents
247 .iter()
248 .map(|content| EncryptedChunk {
249 content: Bytes::copy_from_slice(content),
250 })
251 .collect::<Vec<_>>();
252 self_encryption::decrypt(&data_map, &encrypted_chunks)
253 .map(|bytes| bytes.to_vec())
254 .map_err(|error| BrowserError::SelfEncryption(error.to_string()))
255}
256
257fn chunk_infos(data_map: &DataMap) -> Vec<BrowserChunkInfo> {
258 data_map
259 .infos()
260 .iter()
261 .map(|info| BrowserChunkInfo {
262 index: info.index,
263 dst_hash: hex::encode(info.dst_hash.0),
264 src_hash: hex::encode(info.src_hash.0),
265 src_size: info.src_size,
266 })
267 .collect()
268}
269
270#[cfg(all(target_arch = "wasm32", feature = "browser-wasm"))]
271mod wasm {
272 use super::manifest::parse_browser_manifest;
273 use super::payment::{payment_quote_hash, verify_storage_quote, BrowserQuoteArtifact};
274 use super::protocol::{
275 ice_password_from_sdp, parse_response_frame, parse_webrtc_direct_multiaddr,
276 server_answer_sdp, v2_server_ice_credential, BrowserEndpointInput,
277 };
278 use super::{
279 chunk_infos, content_address, decrypt_public_file, encrypt_public_file, verify_record,
280 BrowserRecord, BrowserRecordInfo, BrowserStagedFile, MAX_BROWSER_FILE_BYTES,
281 };
282 use ant_protocol::transport::{
283 run_iterative_lookup, IterativeLookup, LookupConfig, LookupKey, LookupNode, LookupQuery,
284 LookupQueryOutcome,
285 };
286 use bytes::Bytes;
287 use js_sys::{Array, Function, Promise, Uint8Array};
288 use serde::{Deserialize, Serialize};
289 use std::cell::{Cell, RefCell};
290 use std::collections::HashMap;
291 use std::rc::Rc;
292 use wasm_bindgen::prelude::*;
293 use wasm_bindgen::JsCast;
294 use wasm_bindgen_futures::JsFuture;
295
296 #[derive(Debug, Serialize)]
297 struct BrowserSessionDescription {
298 #[serde(rename = "type")]
299 description_type: &'static str,
300 sdp: String,
301 }
302
303 #[derive(Debug, Clone, Serialize, Deserialize)]
304 #[serde(untagged)]
305 enum BrowserLookupEndpoint {
306 Structured { multiaddr: String },
307 Multiaddr(String),
308 }
309
310 #[derive(Debug, Clone, Serialize, Deserialize)]
311 struct BrowserLookupNode {
312 peer_id: String,
313 #[serde(default)]
314 native_addresses: Vec<String>,
315 #[serde(default)]
316 reliability: f64,
317 #[serde(default)]
318 webrtc_direct: Option<BrowserLookupEndpoint>,
319 }
320
321 #[derive(Debug, Serialize)]
322 struct BrowserLookupBatch {
323 target: String,
324 count: usize,
325 iteration: usize,
326 candidates: Vec<BrowserLookupNode>,
327 }
328
329 #[derive(Debug, Deserialize)]
330 #[serde(tag = "status", rename_all = "snake_case")]
331 enum BrowserLookupQueryOutcome {
332 Succeeded {
333 responder: String,
334 #[serde(default)]
335 candidates: Vec<BrowserLookupNode>,
336 },
337 Failed {
338 responder: String,
339 },
340 Unresponsive {
341 responder: String,
342 },
343 }
344
345 #[derive(Debug, Clone)]
346 struct BrowserLookupCandidate {
347 peer_id: LookupKey,
348 wire: BrowserLookupNode,
349 }
350
351 impl LookupNode for BrowserLookupCandidate {
352 fn lookup_peer_id(&self) -> LookupKey {
353 self.peer_id
354 }
355 }
356
357 impl BrowserLookupCandidate {
358 fn parse(mut wire: BrowserLookupNode) -> Result<Self, JsValue> {
359 let peer_id = parse_lookup_key(&wire.peer_id, "peer ID")?;
360 wire.peer_id = hex::encode(peer_id);
361 Ok(Self { peer_id, wire })
362 }
363 }
364
365 #[wasm_bindgen(js_name = BrowserIterativeLookup)]
367 pub struct BrowserIterativeLookup {
368 lookup: IterativeLookup<BrowserLookupCandidate>,
369 known_endpoints: HashMap<LookupKey, BrowserLookupEndpoint>,
370 }
371
372 #[wasm_bindgen(js_class = BrowserIterativeLookup)]
373 impl BrowserIterativeLookup {
374 #[wasm_bindgen(constructor)]
376 pub fn new(
377 target: &str,
378 count: usize,
379 alpha: usize,
380 max_iterations: usize,
381 ) -> Result<Self, JsValue> {
382 let target = parse_lookup_key(target, "lookup target")?;
383 let config = LookupConfig {
384 count,
385 alpha,
386 max_iterations,
387 ..LookupConfig::saorsa(count)
388 };
389 let lookup = IterativeLookup::new(target, config)
390 .map_err(|error| JsValue::from_str(&error.to_string()))?;
391 Ok(Self {
392 lookup,
393 known_endpoints: HashMap::new(),
394 })
395 }
396
397 #[wasm_bindgen(js_name = addCandidates)]
399 pub fn add_candidates(&mut self, nodes: JsValue) -> Result<(), JsValue> {
400 for candidate in parse_lookup_nodes(nodes)? {
401 self.add_candidate(candidate);
402 }
403 Ok(())
404 }
405
406 #[wasm_bindgen(js_name = run)]
408 pub async fn run(&mut self, query_batch: Function) -> Result<String, JsValue> {
409 let mut query = BrowserLookupQuery {
410 callback: query_batch,
411 known_endpoints: &mut self.known_endpoints,
412 };
413 run_iterative_lookup(
414 &mut self.lookup,
415 &mut query,
416 gloo_timers::future::TimeoutFuture::new(
417 ant_protocol::transport::LOOKUP_TIMEOUT_SECS * 1_000,
418 ),
419 )
420 .await
421 .map(|termination| format!("{termination:?}"))
422 .map_err(|error| JsValue::from_str(&error.to_string()))
423 }
424
425 #[wasm_bindgen(js_name = results)]
427 pub fn results(&self) -> Result<JsValue, JsValue> {
428 let nodes = self
429 .lookup
430 .results()
431 .into_iter()
432 .map(|candidate| candidate.wire)
433 .collect::<Vec<_>>();
434 serde_wasm_bindgen::to_value(&nodes)
435 .map_err(|error| JsValue::from_str(&error.to_string()))
436 }
437
438 #[wasm_bindgen(js_name = queriedPeers)]
440 pub fn queried_peers(&self) -> Result<JsValue, JsValue> {
441 let peers = self
442 .lookup
443 .queried_peers()
444 .iter()
445 .map(hex::encode)
446 .collect::<Vec<_>>();
447 serde_wasm_bindgen::to_value(&peers)
448 .map_err(|error| JsValue::from_str(&error.to_string()))
449 }
450 }
451
452 impl BrowserIterativeLookup {
453 fn add_candidate(&mut self, candidate: BrowserLookupCandidate) {
454 if let Some(candidate) =
455 resolve_candidate_endpoint(&mut self.known_endpoints, candidate)
456 {
457 let _ = self.lookup.add_candidate(candidate);
458 }
459 }
460 }
461
462 struct BrowserLookupQuery<'a> {
463 callback: Function,
464 known_endpoints: &'a mut HashMap<LookupKey, BrowserLookupEndpoint>,
465 }
466
467 impl LookupQuery<BrowserLookupCandidate> for BrowserLookupQuery<'_> {
468 type Error = String;
469
470 async fn query_batch(
471 &mut self,
472 target: LookupKey,
473 count: usize,
474 iteration: usize,
475 batch: Vec<BrowserLookupCandidate>,
476 ) -> Result<Vec<LookupQueryOutcome<BrowserLookupCandidate>>, Self::Error> {
477 let request = BrowserLookupBatch {
478 target: hex::encode(target),
479 count,
480 iteration,
481 candidates: batch.into_iter().map(|candidate| candidate.wire).collect(),
482 };
483 let request = serde_wasm_bindgen::to_value(&request)
484 .map_err(|error| format!("could not encode lookup batch: {error}"))?;
485 let returned = self
486 .callback
487 .call1(&JsValue::NULL, &request)
488 .map_err(js_error_message)?;
489 let returned = JsFuture::from(Promise::resolve(&returned))
490 .await
491 .map_err(js_error_message)?;
492 let outcomes: Vec<BrowserLookupQueryOutcome> = serde_wasm_bindgen::from_value(returned)
493 .map_err(|error| format!("invalid lookup batch response: {error}"))?;
494
495 outcomes
496 .into_iter()
497 .map(|outcome| match outcome {
498 BrowserLookupQueryOutcome::Succeeded {
499 responder,
500 candidates,
501 } => {
502 let responder = parse_lookup_key(&responder, "lookup responder")
503 .map_err(js_error_message)?;
504 let candidates = candidates
505 .into_iter()
506 .map(BrowserLookupCandidate::parse)
507 .collect::<Result<Vec<_>, _>>()
508 .map_err(js_error_message)?
509 .into_iter()
510 .filter_map(|candidate| {
511 resolve_candidate_endpoint(self.known_endpoints, candidate)
512 })
513 .collect();
514 Ok(LookupQueryOutcome::Succeeded {
515 responder,
516 candidates,
517 })
518 }
519 BrowserLookupQueryOutcome::Failed { responder } => {
520 parse_lookup_key(&responder, "lookup responder")
521 .map(|responder| LookupQueryOutcome::Failed { responder })
522 .map_err(js_error_message)
523 }
524 BrowserLookupQueryOutcome::Unresponsive { responder } => {
525 parse_lookup_key(&responder, "lookup responder")
526 .map(|responder| LookupQueryOutcome::Unresponsive { responder })
527 .map_err(js_error_message)
528 }
529 })
530 .collect()
531 }
532 }
533
534 fn resolve_candidate_endpoint(
535 known_endpoints: &mut HashMap<LookupKey, BrowserLookupEndpoint>,
536 mut candidate: BrowserLookupCandidate,
537 ) -> Option<BrowserLookupCandidate> {
538 if let Some(endpoint) = candidate.wire.webrtc_direct.clone() {
539 known_endpoints.insert(candidate.peer_id, endpoint);
540 } else if let Some(endpoint) = known_endpoints.get(&candidate.peer_id) {
541 candidate.wire.webrtc_direct = Some(endpoint.clone());
542 }
543 candidate.wire.webrtc_direct.as_ref()?;
544 Some(candidate)
545 }
546
547 fn js_error_message(value: JsValue) -> String {
548 value
549 .as_string()
550 .unwrap_or_else(|| format!("JavaScript lookup callback failed: {value:?}"))
551 }
552
553 fn parse_lookup_nodes(value: JsValue) -> Result<Vec<BrowserLookupCandidate>, JsValue> {
554 let nodes: Vec<BrowserLookupNode> = serde_wasm_bindgen::from_value(value)
555 .map_err(|error| JsValue::from_str(&format!("invalid lookup nodes: {error}")))?;
556 nodes
557 .into_iter()
558 .map(BrowserLookupCandidate::parse)
559 .collect()
560 }
561
562 fn parse_lookup_key(value: &str, label: &str) -> Result<LookupKey, JsValue> {
563 let value = value.strip_prefix("0x").unwrap_or(value);
564 let bytes = hex::decode(value)
565 .map_err(|error| JsValue::from_str(&format!("invalid {label}: {error}")))?;
566 bytes.try_into().map_err(|bytes: Vec<u8>| {
567 JsValue::from_str(&format!(
568 "invalid {label}: expected 32 bytes, received {}",
569 bytes.len()
570 ))
571 })
572 }
573
574 #[wasm_bindgen(start)]
576 pub fn start() {
577 console_error_panic_hook::set_once();
578 }
579
580 #[wasm_bindgen(js_name = parseWebRtcDirectMultiaddr)]
582 pub fn parse_webrtc_direct_multiaddr_wasm(endpoint: JsValue) -> Result<JsValue, JsValue> {
583 let input: BrowserEndpointInput = serde_wasm_bindgen::from_value(endpoint)
584 .map_err(|error| JsValue::from_str(&error.to_string()))?;
585 let parsed = parse_webrtc_direct_multiaddr(input.multiaddr())
586 .map_err(|error| JsValue::from_str(&error.to_string()))?;
587 serde_wasm_bindgen::to_value(&parsed).map_err(|error| JsValue::from_str(&error.to_string()))
588 }
589
590 #[wasm_bindgen(js_name = parseResponseFrame)]
592 pub fn parse_response_frame_wasm(frame: &[u8]) -> Result<JsValue, JsValue> {
593 let parsed =
594 parse_response_frame(frame).map_err(|error| JsValue::from_str(&error.to_string()))?;
595 parsed
596 .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
597 .map_err(|error| JsValue::from_str(&error.to_string()))
598 }
599
600 #[wasm_bindgen(js_name = serverAnswerFromEndpoint)]
602 pub fn server_answer_from_endpoint_wasm(
603 endpoint: JsValue,
604 ice_credential: &str,
605 ) -> Result<JsValue, JsValue> {
606 let input: BrowserEndpointInput = serde_wasm_bindgen::from_value(endpoint)
607 .map_err(|error| JsValue::from_str(&error.to_string()))?;
608 let endpoint = parse_webrtc_direct_multiaddr(input.multiaddr())
609 .map_err(|error| JsValue::from_str(&error.to_string()))?;
610 let sdp = server_answer_sdp(&endpoint, ice_credential)
611 .map_err(|error| JsValue::from_str(&error.to_string()))?;
612 serde_wasm_bindgen::to_value(&BrowserSessionDescription {
613 description_type: "answer",
614 sdp,
615 })
616 .map_err(|error| JsValue::from_str(&error.to_string()))
617 }
618
619 #[wasm_bindgen(js_name = webRtcDirectV2ServerCredential)]
621 pub fn web_rtc_direct_v2_server_credential_wasm(local_sdp: &str) -> Result<String, JsValue> {
622 let password = ice_password_from_sdp(local_sdp)
623 .map_err(|error| JsValue::from_str(&error.to_string()))?;
624 v2_server_ice_credential(&password).map_err(|error| JsValue::from_str(&error.to_string()))
625 }
626
627 #[wasm_bindgen(js_name = mainnetNetworkDefaults)]
629 pub fn mainnet_network_defaults_wasm() -> Result<JsValue, JsValue> {
630 let defaults = crate::network_defaults::browser_mainnet_defaults()
631 .map_err(|error| JsValue::from_str(&error.to_string()))?;
632 serde_wasm_bindgen::to_value(&defaults)
633 .map_err(|error| JsValue::from_str(&error.to_string()))
634 }
635
636 #[wasm_bindgen(js_name = parseBrowserManifest)]
638 pub fn parse_browser_manifest_wasm(value: JsValue) -> Result<JsValue, JsValue> {
639 let value: serde_json::Value = serde_wasm_bindgen::from_value(value)
640 .map_err(|error| JsValue::from_str(&error.to_string()))?;
641 let manifest =
642 parse_browser_manifest(value).map_err(|error| JsValue::from_str(&error.to_string()))?;
643 serde_wasm_bindgen::to_value(&manifest)
644 .map_err(|error| JsValue::from_str(&error.to_string()))
645 }
646
647 #[wasm_bindgen(js_name = paymentQuoteHash)]
649 #[must_use]
650 pub fn payment_quote_hash_wasm(
651 signed_bytes: &[u8],
652 public_key: &[u8],
653 signature: &[u8],
654 ) -> String {
655 hex::encode(payment_quote_hash(signed_bytes, public_key, signature))
656 }
657
658 #[wasm_bindgen(js_name = verifyStorageQuote)]
660 pub fn verify_storage_quote_wasm(
661 quote: JsValue,
662 expected_address: &str,
663 expected_peer_id: &str,
664 ) -> Result<JsValue, JsValue> {
665 let quote: BrowserQuoteArtifact = serde_wasm_bindgen::from_value(quote)
666 .map_err(|error| JsValue::from_str(&error.to_string()))?;
667 let verified = verify_storage_quote(quote, expected_address, expected_peer_id)
668 .map_err(|error| JsValue::from_str(&error.to_string()))?;
669 serde_wasm_bindgen::to_value(&verified)
670 .map_err(|error| JsValue::from_str(&error.to_string()))
671 }
672
673 #[wasm_bindgen(js_name = decodeMerklePaymentReceipt)]
675 pub fn decode_merkle_payment_receipt(
676 request: JsValue,
677 vault: &str,
678 logs: JsValue,
679 ) -> Result<JsValue, JsValue> {
680 let request = serde_wasm_bindgen::from_value(request)
681 .map_err(|e| JsValue::from_str(&e.to_string()))?;
682 let logs: Vec<super::payment::PaymentLog> =
683 serde_wasm_bindgen::from_value(logs).map_err(|e| JsValue::from_str(&e.to_string()))?;
684 let result = super::payment::decode_merkle_receipt(&request, vault, &logs)
685 .map_err(|e| JsValue::from_str(&e.to_string()))?;
686 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
687 }
688
689 #[wasm_bindgen(js_name = encryptPublicFile)]
691 pub fn encrypt_public_file_wasm(content: &[u8]) -> Result<JsValue, JsValue> {
692 let encrypted =
693 encrypt_public_file(content).map_err(|error| JsValue::from_str(&error.to_string()))?;
694 serde_wasm_bindgen::to_value(&encrypted)
695 .map_err(|error| JsValue::from_str(&error.to_string()))
696 }
697
698 #[wasm_bindgen(js_name = BrowserFileEncryptor)]
704 pub struct BrowserFileEncryptor {
705 stream: self_encryption::EncryptionStream<Box<dyn Iterator<Item = Bytes>>>,
706 file_size: usize,
707 bytes_read: Rc<Cell<usize>>,
708 read_error: Rc<RefCell<Option<String>>>,
709 whole_file_hasher: Rc<RefCell<blake3::Hasher>>,
710 data_map_records: HashMap<[u8; 32], Bytes>,
711 records: Vec<BrowserRecordInfo>,
712 data_map_record_yielded: bool,
713 }
714
715 #[wasm_bindgen(js_class = BrowserFileEncryptor)]
716 impl BrowserFileEncryptor {
717 #[wasm_bindgen(constructor)]
722 pub fn new(file_size: usize, read_chunk: Function) -> Result<Self, JsValue> {
723 if file_size < self_encryption::MIN_ENCRYPTABLE_BYTES {
724 return Err(JsValue::from_str(&format!(
725 "self-encryption requires at least {} bytes",
726 self_encryption::MIN_ENCRYPTABLE_BYTES
727 )));
728 }
729 if file_size > MAX_BROWSER_FILE_BYTES {
730 return Err(JsValue::from_str(&format!(
731 "browser files are limited to {MAX_BROWSER_FILE_BYTES} bytes"
732 )));
733 }
734
735 let bytes_read = Rc::new(Cell::new(0usize));
736 let iterator_bytes_read = Rc::clone(&bytes_read);
737 let read_error = Rc::new(RefCell::new(None));
738 let iterator_error = Rc::clone(&read_error);
739 let whole_file_hasher = Rc::new(RefCell::new(blake3::Hasher::new()));
740 let iterator_hasher = Rc::clone(&whole_file_hasher);
741 let iterator = std::iter::from_fn(move || {
742 if iterator_error.borrow().is_some() {
743 return None;
744 }
745 let offset = iterator_bytes_read.get();
746 if offset >= file_size {
747 return None;
748 }
749 let length = (file_size - offset).min(self_encryption::MAX_CHUNK_SIZE);
750 let returned = match read_chunk.call2(
751 &JsValue::NULL,
752 &JsValue::from_f64(offset as f64),
753 &JsValue::from_f64(length as f64),
754 ) {
755 Ok(returned) => returned,
756 Err(error) => {
757 *iterator_error.borrow_mut() = Some(js_error_message(error));
758 return None;
759 }
760 };
761 if !returned.is_instance_of::<Uint8Array>() {
762 *iterator_error.borrow_mut() = Some(format!(
763 "file reader returned a non-Uint8Array at byte offset {offset}"
764 ));
765 return None;
766 }
767 let returned = Uint8Array::new(&returned);
768 let actual = returned.length() as usize;
769 if actual != length {
770 *iterator_error.borrow_mut() = Some(format!(
771 "file reader returned {actual} bytes at offset {offset}, expected {length}"
772 ));
773 return None;
774 }
775 let mut content = vec![0u8; actual];
776 returned.copy_to(&mut content);
777 iterator_hasher.borrow_mut().update(&content);
778 iterator_bytes_read.set(offset + actual);
779 Some(Bytes::from(content))
780 });
781 let stream = self_encryption::stream_encrypt(
782 file_size,
783 Box::new(iterator) as Box<dyn Iterator<Item = Bytes>>,
784 )
785 .map_err(|error| JsValue::from_str(&error.to_string()))?;
786
787 Ok(Self {
788 stream,
789 file_size,
790 bytes_read,
791 read_error,
792 whole_file_hasher,
793 data_map_records: HashMap::new(),
794 records: Vec::new(),
795 data_map_record_yielded: false,
796 })
797 }
798
799 #[wasm_bindgen(js_name = nextRecord)]
801 pub fn next_record(&mut self) -> Result<JsValue, JsValue> {
802 if self.data_map_record_yielded {
803 return Ok(JsValue::UNDEFINED);
804 }
805
806 let next = self.stream.chunks().next();
807 if let Some(error) = self.read_error.borrow().as_ref() {
808 return Err(JsValue::from_str(error));
809 }
810 if let Some(result) = next {
811 let (hash, content) = result.map_err(|error| {
812 JsValue::from_str(&format!("self-encryption failed: {error}"))
813 })?;
814 if self.stream.datamap().is_some() {
817 self.data_map_records.insert(hash.0, content.clone());
818 }
819 return self.serialize_record(content_address(&content), content.to_vec());
820 }
821
822 let published_data_map = self.stream.datamap().ok_or_else(|| {
823 JsValue::from_str("self-encryption ended before producing a DataMap")
824 })?;
825 let (address, encoded) =
826 crate::client_engine::files::public_map_record(published_data_map)
827 .map_err(|error| JsValue::from_str(&error))?;
828 let address = hex::encode(address);
829 self.data_map_record_yielded = true;
830 self.serialize_record(address, encoded.to_vec())
831 }
832
833 pub fn finish(&self, name: &str, content_type: &str) -> Result<JsValue, JsValue> {
835 if !self.data_map_record_yielded {
836 return Err(JsValue::from_str(
837 "all encrypted records must be staged before finishing",
838 ));
839 }
840 if self.bytes_read.get() != self.file_size {
841 return Err(JsValue::from_str(&format!(
842 "file reader supplied {} bytes, expected {}",
843 self.bytes_read.get(),
844 self.file_size
845 )));
846 }
847 let published_data_map = self
848 .stream
849 .datamap()
850 .ok_or_else(|| JsValue::from_str("self-encryption did not produce a DataMap"))?;
851 let mut get_local_chunk = |address: self_encryption::XorName| {
852 self.data_map_records
853 .get(&address.0)
854 .cloned()
855 .ok_or_else(|| {
856 self_encryption::Error::Generic(format!(
857 "streaming output omitted DataMap chunk {}",
858 hex::encode(address.0)
859 ))
860 })
861 };
862 let root_data_map = self_encryption::get_root_data_map(
863 published_data_map.clone(),
864 &mut get_local_chunk,
865 )
866 .map_err(|error| JsValue::from_str(&format!("self-encryption failed: {error}")))?;
867 let public_record = self.records.last().ok_or_else(|| {
868 JsValue::from_str("self-encryption omitted the public DataMap record")
869 })?;
870 let staged = BrowserStagedFile {
871 name: name.to_string(),
872 content_type: if content_type.is_empty() {
873 "application/octet-stream".to_string()
874 } else {
875 content_type.to_string()
876 },
877 address: public_record.address.clone(),
878 blake3: self
879 .whole_file_hasher
880 .borrow()
881 .clone()
882 .finalize()
883 .to_hex()
884 .to_string(),
885 size: self.file_size,
886 data_map_size: public_record.size,
887 chunks: chunk_infos(&root_data_map),
888 records: self.records.clone(),
889 };
890 serde_wasm_bindgen::to_value(&staged)
891 .map_err(|error| JsValue::from_str(&error.to_string()))
892 }
893 }
894
895 impl BrowserFileEncryptor {
896 fn serialize_record(
897 &mut self,
898 address: String,
899 content: Vec<u8>,
900 ) -> Result<JsValue, JsValue> {
901 self.records.push(BrowserRecordInfo {
902 address: address.clone(),
903 size: content.len(),
904 });
905 serde_wasm_bindgen::to_value(&BrowserRecord { address, content })
906 .map_err(|error| JsValue::from_str(&error.to_string()))
907 }
908 }
909
910 #[wasm_bindgen(js_name = contentAddress)]
912 #[must_use]
913 pub fn content_address_wasm(content: &[u8]) -> String {
914 content_address(content)
915 }
916
917 #[wasm_bindgen(js_name = verifyRecord)]
919 pub fn verify_record_wasm(address: &str, content: &[u8]) -> Result<String, JsValue> {
920 verify_record(address, content).map_err(|error| JsValue::from_str(&error.to_string()))?;
921 Ok(content_address(content))
922 }
923
924 #[wasm_bindgen(js_name = decodePublicDataMap)]
926 pub fn decode_public_data_map_wasm(content: &[u8]) -> Result<JsValue, JsValue> {
927 let chunks = super::decode_public_data_map(content)
928 .map_err(|error| JsValue::from_str(&error.to_string()))?;
929 serde_wasm_bindgen::to_value(&chunks).map_err(|error| JsValue::from_str(&error.to_string()))
930 }
931
932 #[wasm_bindgen(js_name = decryptPublicFile)]
934 pub fn decrypt_public_file_wasm(
935 data_map_content: &[u8],
936 encrypted_contents: Array,
937 ) -> Result<Uint8Array, JsValue> {
938 let encrypted_contents = encrypted_contents
939 .iter()
940 .map(|value| Uint8Array::new(&value).to_vec())
941 .collect::<Vec<_>>();
942 let plaintext = decrypt_public_file(data_map_content, &encrypted_contents)
943 .map_err(|error| JsValue::from_str(&error.to_string()))?;
944 Ok(Uint8Array::from(plaintext.as_slice()))
945 }
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951
952 fn fixture() -> Vec<u8> {
953 "browser whole-file fixture\n".repeat(160).into_bytes()
954 }
955
956 #[test]
957 fn native_browser_encrypt_matches_existing_wire_vector() {
958 let encrypted = encrypt_public_file(&fixture()).expect("encrypt fixture");
959 assert_eq!(
960 encrypted
961 .chunks
962 .iter()
963 .map(|chunk| chunk.dst_hash.as_str())
964 .collect::<Vec<_>>(),
965 vec![
966 "c024c6884a2f39be7ba07c3d9636efedeb94df7397fcd38bac5ae904643c5cc9",
967 "350a88e6eb0b2a3e774107a212a272b4191af69ca4366a4b91f5a1e5872c459a",
968 "d73db5a8b0be3b571b40d2b80ff490fe45e135f1992c5863ecb78e25d00ceddb",
969 ]
970 );
971 assert_eq!(
972 encrypted.address,
973 "0d3636dd504d04a236f7e104909234766f077fa7e1ca4a18293d3d168d5f169b"
974 );
975 assert_eq!(
976 encrypted.blake3,
977 "e0e422267ac59c56bf032d6d830035d343369d20147dd5f6b63351a29b015f22"
978 );
979 }
980
981 #[test]
982 fn native_browser_round_trip_and_tamper_rejection() {
983 let content = fixture();
984 let encrypted = encrypt_public_file(&content).expect("encrypt fixture");
985 let data_map = &encrypted.records.last().expect("DataMap record").content;
986 let chunks = encrypted.records[..encrypted.records.len() - 1]
987 .iter()
988 .map(|record| record.content.clone())
989 .collect::<Vec<_>>();
990 assert_eq!(
991 decrypt_public_file(data_map, &chunks).expect("decrypt fixture"),
992 content
993 );
994
995 let mut tampered = chunks;
996 tampered[0][0] ^= 1;
997 assert!(decrypt_public_file(data_map, &tampered).is_err());
998 }
999
1000 #[test]
1001 fn nested_data_map_round_trip() {
1002 let size = 3 * self_encryption::MAX_CHUNK_SIZE + 1;
1003 let content = (0..size).map(|index| index as u8).collect::<Vec<_>>();
1004 let encrypted = encrypt_public_file(&content).expect("encrypt nested fixture");
1005 assert_eq!(encrypted.chunks.len(), 4);
1006 assert!(encrypted.records.len() > encrypted.chunks.len() + 1);
1007
1008 let data_map = &encrypted.records.last().expect("DataMap record").content;
1009 let published: DataMap = rmp_serde::from_slice(data_map).expect("decode published map");
1010 assert!(published.is_child());
1011
1012 let mut required_addresses = decode_public_data_map(data_map)
1013 .expect("decode child map")
1014 .into_iter()
1015 .map(|chunk| chunk.dst_hash)
1016 .collect::<HashSet<_>>();
1017 required_addresses.extend(encrypted.chunks.iter().map(|chunk| chunk.dst_hash.clone()));
1018 let records = encrypted.records[..encrypted.records.len() - 1]
1019 .iter()
1020 .filter(|record| required_addresses.contains(&record.address))
1021 .map(|record| record.content.clone())
1022 .collect::<Vec<_>>();
1023 assert_eq!(records.len(), encrypted.records.len() - 1);
1024 assert_eq!(
1025 decrypt_public_file(data_map, &records).expect("decrypt nested fixture"),
1026 content
1027 );
1028 }
1029}