aboutsummaryrefslogtreecommitdiff
path: root/import/asset_token/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'import/asset_token/src/lib.rs')
-rw-r--r--import/asset_token/src/lib.rs105
1 files changed, 105 insertions, 0 deletions
diff --git a/import/asset_token/src/lib.rs b/import/asset_token/src/lib.rs
new file mode 100644
index 0000000..87ea261
--- /dev/null
+++ b/import/asset_token/src/lib.rs
@@ -0,0 +1,105 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2025 metamuffin <metamuffin.org>
+*/
+pub mod assetfed;
+
+use aes_gcm_siv::{
+ aead::{generic_array::GenericArray, Aead},
+ Aes256GcmSiv, KeyInit,
+};
+use anyhow::{anyhow, bail, Context};
+use base64::Engine;
+use bincode::{Decode, Encode};
+use jellycache::CachePath;
+pub use jellycommon as common;
+use jellycommon::{Asset, LocalTrack};
+use log::warn;
+use serde::{Deserialize, Serialize};
+use std::sync::Mutex;
+use std::{path::PathBuf, sync::LazyLock};
+
+#[rustfmt::skip]
+#[derive(Debug, Deserialize, Serialize, Default)]
+pub struct Config {
+ asset_key: Option<String>,
+}
+
+pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None);
+static CONF: LazyLock<Config> = LazyLock::new(|| {
+ CONF_PRELOAD
+ .lock()
+ .unwrap()
+ .take()
+ .expect("cache config not preloaded. logic error")
+});
+
+const VERSION: u32 = 3;
+
+static ASSET_KEY: LazyLock<Aes256GcmSiv> = LazyLock::new(|| {
+ if let Some(sk) = &CONF.asset_key {
+ let r = base64::engine::general_purpose::STANDARD
+ .decode(sk)
+ .expect("key invalid; should be valid base64");
+ aes_gcm_siv::Aes256GcmSiv::new_from_slice(&r)
+ .expect("key has the wrong length; should be 32 bytes")
+ } else {
+ warn!("session_key not configured; generating a random one.");
+ aes_gcm_siv::Aes256GcmSiv::new_from_slice(&[(); 32].map(|_| rand::random())).unwrap()
+ }
+});
+
+#[derive(Debug, Encode, Decode, Serialize)]
+pub enum AssetInner {
+ Federated { host: String, asset: Vec<u8> },
+ Cache(CachePath),
+ Assets(PathBuf),
+ Media(PathBuf),
+ LocalTrack(LocalTrack),
+}
+
+impl AssetInner {
+ pub fn ser(&self) -> Asset {
+ let mut plaintext = Vec::new();
+ plaintext.extend(u32::to_le_bytes(VERSION));
+ plaintext.extend(bincode::encode_to_vec(self, bincode::config::standard()).unwrap());
+
+ while plaintext.len() % 16 == 0 {
+ plaintext.push(0);
+ }
+
+ let nonce = [(); 12].map(|_| rand::random());
+ let mut ciphertext = ASSET_KEY
+ .encrypt(&GenericArray::from(nonce), plaintext.as_slice())
+ .unwrap();
+ ciphertext.extend(nonce);
+
+ Asset(base64::engine::general_purpose::URL_SAFE.encode(&ciphertext))
+ }
+ pub fn deser(s: &str) -> anyhow::Result<Self> {
+ let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(s)?;
+ let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12);
+ let plaintext = ASSET_KEY
+ .decrypt(nonce.into(), ciphertext)
+ .map_err(|_| anyhow!("asset token decrypt failed"))?;
+
+ let version = u32::from_le_bytes(plaintext[0..4].try_into().unwrap());
+ if version != VERSION {
+ bail!("asset token version mismatch");
+ }
+
+ let (data, _): (AssetInner, _) =
+ bincode::decode_from_slice(&plaintext[4..], bincode::config::standard())
+ .context("asset token has invalid format")?;
+ Ok(data)
+ }
+
+ /// Returns `true` if the asset inner is [`Federated`].
+ ///
+ /// [`Federated`]: AssetInner::Federated
+ #[must_use]
+ pub fn is_federated(&self) -> bool {
+ matches!(self, Self::Federated { .. })
+ }
+}