diff options
Diffstat (limited to 'import/asset_token/src/lib.rs')
-rw-r--r-- | import/asset_token/src/lib.rs | 105 |
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 { .. }) + } +} |