1use std::fmt;
29
30use aes_gcm::{
31 aead::{Aead, KeyInit, OsRng},
32 Aes256Gcm, Key, Nonce,
33};
34use futures_core::Stream;
35use hex::{decode as hex_decode, encode as hex_encode};
36use serde::{Deserialize, Serialize};
37use sha2::{Digest, Sha256};
38
39pub use self::stream::StreamHandle;
42
43const DEFAULT_BASE: &str = "https://voidnote.net";
44
45#[derive(Debug)]
49pub enum Error {
50 InvalidToken,
52 MissingApiKey,
54 Network(reqwest::Error),
56 Http { status: u16, body: String },
58 Json(serde_json::Error),
60 DecryptionFailed,
62 InvalidHex(hex::FromHexError),
64}
65
66impl fmt::Display for Error {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Error::InvalidToken => write!(f, "invalid token: expected 64-char hex string"),
70 Error::MissingApiKey => write!(f, "api_key is required"),
71 Error::Network(e) => write!(f, "network error: {e}"),
72 Error::Http { status, body } => write!(f, "HTTP {status}: {body}"),
73 Error::Json(e) => write!(f, "JSON parse error: {e}"),
74 Error::DecryptionFailed => write!(f, "decryption failed: wrong key or tampered data"),
75 Error::InvalidHex(e) => write!(f, "invalid hex: {e}"),
76 }
77 }
78}
79
80impl std::error::Error for Error {}
81
82impl From<reqwest::Error> for Error {
83 fn from(e: reqwest::Error) -> Self {
84 Error::Network(e)
85 }
86}
87
88impl From<serde_json::Error> for Error {
89 fn from(e: serde_json::Error) -> Self {
90 Error::Json(e)
91 }
92}
93
94impl From<hex::FromHexError> for Error {
95 fn from(e: hex::FromHexError) -> Self {
96 Error::InvalidHex(e)
97 }
98}
99
100pub type Result<T> = std::result::Result<T, Error>;
102
103#[derive(Debug, Clone)]
107pub struct ReadResult {
108 pub content: String,
109 pub title: Option<String>,
110 pub view_count: u32,
111 pub max_views: u32,
112 pub destroyed: bool,
114}
115
116#[derive(Debug, Clone, Default)]
118pub struct CreateOptions {
119 pub api_key: String,
121 pub title: Option<String>,
122 pub max_views: Option<u8>,
124 pub expires_in: Option<u16>,
126 pub note_type: Option<String>,
128 pub base: Option<String>,
130}
131
132#[derive(Debug, Clone)]
134pub struct CreateResult {
135 pub url: String,
137 pub expires_at: String,
138}
139
140#[derive(Debug, Clone, Default)]
142pub struct StreamOptions {
143 pub api_key: String,
145 pub title: Option<String>,
146 pub ttl: Option<u32>,
148 pub base: Option<String>,
150}
151
152pub async fn read(url_or_token: &str) -> Result<ReadResult> {
159 read_from(url_or_token, DEFAULT_BASE).await
160}
161
162pub async fn read_from(url_or_token: &str, base: &str) -> Result<ReadResult> {
164 let token = extract_token(url_or_token)?;
165 let token_id = &token[..32];
166 let secret = &token[32..];
167
168 let url = format!("{base}/api/note/{token_id}");
169 let resp = reqwest::get(&url).await?;
170
171 let status = resp.status().as_u16();
172 let body = resp.text().await?;
173
174 if status == 404 {
175 return Err(Error::Http {
176 status,
177 body: "note not found or already destroyed".into(),
178 });
179 }
180 if status != 200 {
181 return Err(Error::Http { status, body });
182 }
183
184 #[derive(Deserialize)]
186 struct RawResponse {
187 encrypted_content: Option<String>,
188 iv: String,
189 title: Option<String>,
190 #[serde(alias = "viewCount", default)]
192 view_count: u32,
193 #[serde(alias = "maxViews", default)]
194 max_views: u32,
195 #[serde(default)]
196 destroyed: bool,
197 }
198
199 let raw: RawResponse = serde_json::from_str(&body)?;
200 let enc_hex = raw.encrypted_content.as_deref().unwrap_or_default();
201
202 let content = decrypt_content(enc_hex, &raw.iv, secret)?;
203
204 Ok(ReadResult {
205 content,
206 title: raw.title,
207 view_count: raw.view_count,
208 max_views: raw.max_views,
209 destroyed: raw.destroyed,
210 })
211}
212
213pub async fn create(content: &str, opts: CreateOptions) -> Result<CreateResult> {
216 if opts.api_key.is_empty() {
217 return Err(Error::MissingApiKey);
218 }
219 let base = opts.base.as_deref().unwrap_or(DEFAULT_BASE);
220
221 let (full_token, token_id, secret) = generate_token();
222 let (enc_hex, iv_hex) = encrypt_content(content, &secret)?;
223
224 let max_views = opts.max_views.unwrap_or(1);
225
226 #[derive(Serialize)]
227 #[serde(rename_all = "camelCase")]
228 struct CreateBody<'a> {
229 token_id: &'a str,
230 encrypted_content: &'a str,
231 iv: &'a str,
232 max_views: u8,
233 title: Option<&'a str>,
234 expires_in: u16,
235 note_type: &'a str,
236 }
237
238 let expires_in = opts.expires_in.unwrap_or(24);
239 let note_type_str = opts.note_type.as_deref().unwrap_or("secure").to_owned();
240
241 let payload = CreateBody {
242 token_id: &token_id,
243 encrypted_content: &enc_hex,
244 iv: &iv_hex,
245 max_views,
246 title: opts.title.as_deref(),
247 expires_in,
248 note_type: ¬e_type_str,
249 };
250
251 let client = reqwest::Client::new();
252 let resp = client
253 .post(format!("{base}/api/notes"))
254 .bearer_auth(&opts.api_key)
255 .json(&payload)
256 .send()
257 .await?;
258
259 let status = resp.status().as_u16();
260 let body = resp.text().await?;
261
262 if status != 200 && status != 201 {
263 return Err(Error::Http { status, body });
264 }
265
266 #[derive(Deserialize, Default)]
267 #[serde(default)]
268 struct CreateResponse {
269 #[serde(rename = "siteUrl")]
270 site_url: Option<String>,
271 #[serde(rename = "expiresAt", alias = "expires_at")]
272 expires_at: Option<String>,
273 }
274
275 let raw: CreateResponse = serde_json::from_str(&body).unwrap_or_default();
276 let site = raw.site_url.as_deref().unwrap_or(base);
277
278 Ok(CreateResult {
279 url: format!("{site}/note/{full_token}"),
280 expires_at: raw.expires_at.unwrap_or_default(),
281 })
282}
283
284pub async fn create_stream(opts: StreamOptions) -> Result<StreamHandle> {
288 if opts.api_key.is_empty() {
289 return Err(Error::MissingApiKey);
290 }
291 let base = opts.base.unwrap_or_else(|| DEFAULT_BASE.into());
292 let ttl = opts.ttl.unwrap_or(3600);
293
294 let (full_token, token_id, secret) = generate_token();
295
296 #[derive(Serialize)]
297 #[serde(rename_all = "camelCase")]
298 struct StreamBody<'a> {
299 token_id: &'a str,
300 title: Option<&'a str>,
301 ttl: u32,
302 }
303
304 let payload = StreamBody {
305 token_id: &token_id,
306 title: opts.title.as_deref(),
307 ttl,
308 };
309
310 let client = reqwest::Client::new();
311 let resp = client
312 .post(format!("{base}/api/stream"))
313 .bearer_auth(&opts.api_key)
314 .json(&payload)
315 .send()
316 .await?;
317
318 let status = resp.status().as_u16();
319 let body = resp.text().await?;
320
321 if status != 200 && status != 201 {
322 return Err(Error::Http { status, body });
323 }
324
325 #[derive(Deserialize, Default)]
326 #[serde(default)]
327 struct StreamResponse {
328 #[serde(rename = "siteUrl")]
329 site_url: Option<String>,
330 #[serde(rename = "expiresAt")]
331 expires_at: Option<String>,
332 }
333
334 let raw: StreamResponse = serde_json::from_str(&body).unwrap_or_default();
335 let site = raw.site_url.as_deref().unwrap_or(&base);
336
337 let key = derive_key_bytes(&secret)?;
339
340 Ok(StreamHandle {
341 url: format!("{site}/stream/{full_token}"),
342 expires_at: raw.expires_at.unwrap_or_default(),
343 full_token,
344 secret,
345 key,
346 base,
347 client: reqwest::Client::new(),
348 })
349}
350
351pub mod stream {
354 use super::*;
355
356 pub struct StreamHandle {
358 pub url: String,
360 pub expires_at: String,
361 pub(crate) full_token: String,
362 pub(crate) secret: String,
363 pub(crate) key: [u8; 32], pub(crate) base: String,
365 pub(crate) client: reqwest::Client,
366 }
367
368 impl StreamHandle {
369 pub async fn write(&self, content: &str) -> Result<()> {
371 let (enc_hex, iv_hex) = encrypt_content(content, &self.secret)?;
372
373 #[derive(Serialize)]
374 #[serde(rename_all = "camelCase")]
375 struct WriteBody<'a> {
376 encrypted_content: &'a str,
377 iv: &'a str,
378 }
379
380 let resp = self
381 .client
382 .post(format!(
383 "{}/api/stream/{}/write",
384 self.base, self.full_token
385 ))
386 .json(&WriteBody {
387 encrypted_content: &enc_hex,
388 iv: &iv_hex,
389 })
390 .send()
391 .await?;
392
393 if !resp.status().is_success() {
394 let status = resp.status().as_u16();
395 let body = resp.text().await.unwrap_or_default();
396 return Err(Error::Http { status, body });
397 }
398 Ok(())
399 }
400
401 pub async fn close(&self) -> Result<()> {
403 let resp = self
404 .client
405 .post(format!(
406 "{}/api/stream/{}/close",
407 self.base, self.full_token
408 ))
409 .json(&serde_json::json!({}))
410 .send()
411 .await?;
412
413 if !resp.status().is_success() {
414 let status = resp.status().as_u16();
415 let body = resp.text().await.unwrap_or_default();
416 return Err(Error::Http { status, body });
417 }
418 Ok(())
419 }
420
421 pub fn watch(&self) -> impl Stream<Item = Result<String>> + Send + 'static {
435 let base = self.base.clone();
436 let full_token = self.full_token.clone();
437 let key = self.key;
438 let client = self.client.clone();
439
440 async_stream::stream! {
441 let mut last_id: Option<String> = None;
442
443 'outer: loop {
444 let mut req = client
445 .get(format!("{base}/api/stream/{full_token}/events"));
446
447 if let Some(ref id) = last_id {
448 req = req.header("Last-Event-ID", id);
449 }
450
451 let resp = match req.send().await {
452 Ok(r) => r,
453 Err(e) => {
454 yield Err(Error::Network(e));
455 return;
456 }
457 };
458
459 if !resp.status().is_success() {
460 yield Err(Error::Http {
461 status: resp.status().as_u16(),
462 body: resp.text().await.unwrap_or_default(),
463 });
464 return;
465 }
466
467 use futures::StreamExt as _;
468 let mut byte_stream = resp.bytes_stream();
469
470 let mut buf = String::new();
471 let mut event_id = String::new();
472 let mut event_data = String::new();
473
474 while let Some(chunk) = byte_stream.next().await {
475 let chunk = match chunk {
476 Ok(c) => c,
477 Err(_) => break, };
479
480 let text = match std::str::from_utf8(&chunk) {
481 Ok(s) => s,
482 Err(_) => continue,
483 };
484
485 buf.push_str(text);
486
487 while let Some(nl_pos) = buf.find('\n') {
489 let raw_line = &buf[..nl_pos];
490 let line = raw_line.trim_end_matches('\r');
491 let owned_line = line.to_owned();
492 buf.drain(..nl_pos + 1);
493
494 if let Some(id) = owned_line.strip_prefix("id: ") {
495 event_id = id.to_owned();
496 } else if let Some(data) = owned_line.strip_prefix("data: ") {
497 event_data = data.to_owned();
498 } else if owned_line.is_empty() && !event_data.is_empty() {
499 if !event_id.is_empty() {
501 last_id = Some(event_id.clone());
502 event_id.clear();
503 }
504
505 let data = event_data.clone();
506 event_data.clear();
507
508 #[derive(Deserialize)]
509 struct SseEvent {
510 #[serde(rename = "type")]
511 event_type: Option<String>,
512 enc: Option<String>,
513 iv: Option<String>,
514 }
515
516 let evt: SseEvent = match serde_json::from_str(&data) {
517 Ok(e) => e,
518 Err(_) => continue,
519 };
520
521 if let Some(ref t) = evt.event_type {
522 if t == "closed" || t == "expired" {
523 return; }
525 }
526
527 if let (Some(enc_hex), Some(iv_hex)) = (evt.enc, evt.iv) {
528 match decrypt_with_key(&enc_hex, &iv_hex, &key) {
529 Ok(plaintext) => yield Ok(plaintext),
530 Err(_) => continue, }
532 }
533 }
534 }
535 }
536 continue 'outer;
538 }
539 }
540 }
541 }
542
543 impl fmt::Debug for StreamHandle {
544 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
545 f.debug_struct("StreamHandle")
546 .field("url", &self.url)
547 .field("expires_at", &self.expires_at)
548 .finish()
549 }
550 }
551}
552
553#[derive(Debug, Clone)]
557pub struct CryptoOrderOptions {
558 pub api_key: String,
560 pub credits: u32,
562 pub currency: String,
564 pub base: Option<String>,
566}
567
568#[derive(Debug, Clone, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct CryptoOrder {
572 pub order_id: String,
574 pub address: String,
576 pub amount: String,
578 pub currency: String,
580 pub credits: u32,
582 pub expires_at: String,
584}
585
586#[derive(Debug, Clone)]
588pub struct SubmitPaymentOptions {
589 pub api_key: String,
591 pub order_id: String,
593 pub tx_hash: String,
595 pub base: Option<String>,
597}
598
599#[derive(Debug, Clone, Deserialize)]
601#[serde(rename_all = "camelCase")]
602pub struct SubmitPaymentResult {
603 pub success: bool,
605 pub message: String,
607 pub status: String,
609}
610
611pub async fn create_crypto_order(opts: CryptoOrderOptions) -> Result<CryptoOrder> {
615 if opts.api_key.is_empty() {
616 return Err(Error::MissingApiKey);
617 }
618 let base = opts.base.as_deref().unwrap_or(DEFAULT_BASE);
619
620 #[derive(Serialize)]
621 #[serde(rename_all = "camelCase")]
622 struct OrderBody {
623 credits: u32,
624 currency: String,
625 }
626
627 let payload = OrderBody {
628 credits: opts.credits,
629 currency: opts.currency.clone(),
630 };
631
632 let client = reqwest::Client::new();
633 let resp = client
634 .post(format!("{base}/api/buy/crypto/create-order"))
635 .bearer_auth(&opts.api_key)
636 .json(&payload)
637 .send()
638 .await?;
639
640 let status = resp.status().as_u16();
641 let body = resp.text().await?;
642
643 if status != 200 && status != 201 {
644 return Err(Error::Http { status, body });
645 }
646
647 let order: CryptoOrder = serde_json::from_str(&body)?;
648 Ok(order)
649}
650
651pub async fn submit_crypto_payment(opts: SubmitPaymentOptions) -> Result<SubmitPaymentResult> {
656 if opts.api_key.is_empty() {
657 return Err(Error::MissingApiKey);
658 }
659 let base = opts.base.as_deref().unwrap_or(DEFAULT_BASE);
660
661 #[derive(Serialize)]
662 #[serde(rename_all = "camelCase")]
663 struct SubmitBody {
664 order_id: String,
665 tx_hash: String,
666 }
667
668 let payload = SubmitBody {
669 order_id: opts.order_id.clone(),
670 tx_hash: opts.tx_hash.clone(),
671 };
672
673 let client = reqwest::Client::new();
674 let resp = client
675 .post(format!("{base}/api/buy/crypto/submit-tx"))
676 .bearer_auth(&opts.api_key)
677 .json(&payload)
678 .send()
679 .await?;
680
681 let status = resp.status().as_u16();
682 let body = resp.text().await?;
683
684 if status != 200 && status != 201 {
685 return Err(Error::Http { status, body });
686 }
687
688 let result: SubmitPaymentResult = serde_json::from_str(&body)?;
689 Ok(result)
690}
691
692pub mod blocking {
697 use super::*;
698
699 fn rt() -> tokio::runtime::Runtime {
700 tokio::runtime::Runtime::new().expect("failed to create tokio runtime")
701 }
702
703 pub fn read(url_or_token: &str) -> Result<ReadResult> {
705 rt().block_on(super::read(url_or_token))
706 }
707
708 pub fn create(content: &str, opts: CreateOptions) -> Result<CreateResult> {
710 rt().block_on(super::create(content, opts))
711 }
712
713 pub fn create_stream(opts: StreamOptions) -> Result<StreamHandle> {
715 rt().block_on(super::create_stream(opts))
716 }
717
718 pub fn create_crypto_order(opts: CryptoOrderOptions) -> Result<CryptoOrder> {
720 rt().block_on(super::create_crypto_order(opts))
721 }
722
723 pub fn submit_crypto_payment(opts: SubmitPaymentOptions) -> Result<SubmitPaymentResult> {
725 rt().block_on(super::submit_crypto_payment(opts))
726 }
727}
728
729fn generate_token() -> (String, String, String) {
733 use aes_gcm::aead::rand_core::RngCore;
734 let mut raw = [0u8; 32];
735 OsRng.fill_bytes(&mut raw);
736 let full = hex_encode(raw);
737 let token_id = full[..32].to_owned();
738 let secret = full[32..].to_owned();
739 (full, token_id, secret)
740}
741
742fn derive_key_bytes(secret_hex: &str) -> Result<[u8; 32]> {
744 let secret_bytes = hex_decode(secret_hex)?;
745 let hash = Sha256::digest(&secret_bytes);
746 let mut key = [0u8; 32];
747 key.copy_from_slice(&hash);
748 Ok(key)
749}
750
751fn encrypt_content(plaintext: &str, secret_hex: &str) -> Result<(String, String)> {
754 use aes_gcm::aead::rand_core::RngCore;
755
756 let key_bytes = derive_key_bytes(secret_hex)?;
757 let key = Key::<Aes256Gcm>::from_slice(&key_bytes);
758 let cipher = Aes256Gcm::new(key);
759
760 let mut iv_bytes = [0u8; 12];
761 OsRng.fill_bytes(&mut iv_bytes);
762 let nonce = Nonce::from_slice(&iv_bytes);
763
764 let ciphertext = cipher
766 .encrypt(nonce, plaintext.as_bytes())
767 .map_err(|_| Error::DecryptionFailed)?;
768
769 Ok((hex_encode(&ciphertext), hex_encode(iv_bytes)))
770}
771
772fn decrypt_content(enc_hex: &str, iv_hex: &str, secret_hex: &str) -> Result<String> {
774 let key_bytes = derive_key_bytes(secret_hex)?;
775 decrypt_with_key(enc_hex, iv_hex, &key_bytes)
776}
777
778fn decrypt_with_key(enc_hex: &str, iv_hex: &str, key: &[u8; 32]) -> Result<String> {
780 let ct_with_tag = hex_decode(enc_hex)?;
781 let iv_bytes = hex_decode(iv_hex)?;
782
783 if iv_bytes.len() != 12 {
784 return Err(Error::DecryptionFailed);
785 }
786
787 let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
788 let nonce = Nonce::from_slice(&iv_bytes);
789
790 let plaintext = cipher
791 .decrypt(nonce, ct_with_tag.as_ref())
792 .map_err(|_| Error::DecryptionFailed)?;
793
794 String::from_utf8(plaintext).map_err(|_| Error::DecryptionFailed)
795}
796
797fn extract_token(url_or_token: &str) -> Result<String> {
799 let s = if url_or_token.starts_with("http") {
800 url_or_token
801 .trim_end_matches('/')
802 .split('/')
803 .last()
804 .unwrap_or("")
805 } else {
806 url_or_token
807 };
808
809 if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
810 return Err(Error::InvalidToken);
811 }
812 Ok(s.to_lowercase())
813}
814
815#[cfg(test)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn test_encrypt_decrypt_roundtrip() {
823 let (_, _, secret) = generate_token();
824 let plaintext = "Hello, VoidNote!";
825 let (enc, iv) = encrypt_content(plaintext, &secret).unwrap();
826 let decrypted = decrypt_content(&enc, &iv, &secret).unwrap();
827 assert_eq!(plaintext, decrypted);
828 }
829
830 #[test]
831 fn test_token_generation_format() {
832 let (full, id, secret) = generate_token();
833 assert_eq!(full.len(), 64);
834 assert_eq!(id.len(), 32);
835 assert_eq!(secret.len(), 32);
836 assert_eq!(&full[..32], id);
837 assert_eq!(&full[32..], secret);
838 assert!(full.chars().all(|c| c.is_ascii_hexdigit()));
839 }
840
841 #[test]
842 fn test_extract_token_from_url() {
843 let token = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
844 let url = format!("https://voidnote.net/note/{token}");
845 let extracted = extract_token(&url).unwrap();
846 assert_eq!(token, extracted);
847 }
848
849 #[test]
850 fn test_extract_token_raw() {
851 let token = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
852 let extracted = extract_token(token).unwrap();
853 assert_eq!(token, extracted);
854 }
855
856 #[test]
857 fn test_extract_token_invalid() {
858 assert!(extract_token("tooshort").is_err());
859 assert!(extract_token("zz000000000000000000000000000000000000000000000000000000000000000").is_err());
860 }
861
862 #[test]
863 fn test_key_derivation_deterministic() {
864 let secret = "aabbccddeeff00112233445566778899";
865 let k1 = derive_key_bytes(secret).unwrap();
866 let k2 = derive_key_bytes(secret).unwrap();
867 assert_eq!(k1, k2);
868 }
869
870 #[test]
871 fn test_decryption_wrong_key_fails() {
872 let (_, _, secret) = generate_token();
873 let (_, _, wrong_secret) = generate_token();
874 let (enc, iv) = encrypt_content("secret", &secret).unwrap();
875 assert!(decrypt_content(&enc, &iv, &wrong_secret).is_err());
876 }
877
878 #[test]
879 fn test_tampered_ciphertext_fails() {
880 let (_, _, secret) = generate_token();
881 let (mut enc, iv) = encrypt_content("secret", &secret).unwrap();
882 let last = enc.len() - 1;
884 enc.replace_range(last.., if enc.ends_with('f') { "0" } else { "f" });
885 assert!(decrypt_content(&enc, &iv, &secret).is_err());
886 }
887}