atagen

malice

public
Star0
Watch0
afba1feb41cebde51e0dd74fa483474fbf4d06ac
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-push/src/ece.rs

aa3dce5114d0atagen2026-10-26 06:09 UTC1
//! RFC 8291 message encryption with the aes128gcm content coding of RFC 8188,
aa3dce5114d0atagen2026-10-26 06:09 UTC2
//! as a single record.
aa3dce5114d0atagen2026-10-26 06:09 UTC3
aa3dce5114d0atagen2026-10-26 06:09 UTC4
use aws_lc_rs::{
aa3dce5114d0atagen2026-10-26 06:09 UTC5
    aead::{AES_128_GCM, Aad, LessSafeKey, Nonce, UnboundKey},
aa3dce5114d0atagen2026-10-26 06:09 UTC6
    agreement::{self, ECDH_P256, PrivateKey, UnparsedPublicKey},
aa3dce5114d0atagen2026-10-26 06:09 UTC7
    hkdf::{HKDF_SHA256, KeyType, Salt},
aa3dce5114d0atagen2026-10-26 06:09 UTC8
};
aa3dce5114d0atagen2026-10-26 06:09 UTC9
aa3dce5114d0atagen2026-10-26 06:09 UTC10
const RECORD_SIZE: u32 = 4096;
aa3dce5114d0atagen2026-10-26 06:09 UTC11
const TAG_LEN: usize = 16;
aa3dce5114d0atagen2026-10-26 06:09 UTC12
const HEADER_LEN: usize = 16 + 4 + 1 + 65;
aa3dce5114d0atagen2026-10-26 06:09 UTC13
/// Push services cap the encrypted body at 4096 bytes.
aa3dce5114d0atagen2026-10-26 06:09 UTC14
pub const MAX_PLAINTEXT: usize = 4096 - HEADER_LEN - TAG_LEN - 1;
aa3dce5114d0atagen2026-10-26 06:09 UTC15
aa3dce5114d0atagen2026-10-26 06:09 UTC16
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
aa3dce5114d0atagen2026-10-26 06:09 UTC17
pub enum EceError {
aa3dce5114d0atagen2026-10-26 06:09 UTC18
    BadKey,
aa3dce5114d0atagen2026-10-26 06:09 UTC19
    TooLong,
aa3dce5114d0atagen2026-10-26 06:09 UTC20
    Crypto,
aa3dce5114d0atagen2026-10-26 06:09 UTC21
}
aa3dce5114d0atagen2026-10-26 06:09 UTC22
aa3dce5114d0atagen2026-10-26 06:09 UTC23
impl std::fmt::Display for EceError {
aa3dce5114d0atagen2026-10-26 06:09 UTC24
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
aa3dce5114d0atagen2026-10-26 06:09 UTC25
        f.write_str(match self {
aa3dce5114d0atagen2026-10-26 06:09 UTC26
            Self::BadKey => "the subscription's keys are not valid",
aa3dce5114d0atagen2026-10-26 06:09 UTC27
            Self::TooLong => "the push payload is too long",
aa3dce5114d0atagen2026-10-26 06:09 UTC28
            Self::Crypto => "push encryption failed",
aa3dce5114d0atagen2026-10-26 06:09 UTC29
        })
aa3dce5114d0atagen2026-10-26 06:09 UTC30
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC31
}
aa3dce5114d0atagen2026-10-26 06:09 UTC32
aa3dce5114d0atagen2026-10-26 06:09 UTC33
impl std::error::Error for EceError {}
aa3dce5114d0atagen2026-10-26 06:09 UTC34
aa3dce5114d0atagen2026-10-26 06:09 UTC35
struct Len(usize);
aa3dce5114d0atagen2026-10-26 06:09 UTC36
aa3dce5114d0atagen2026-10-26 06:09 UTC37
impl KeyType for Len {
aa3dce5114d0atagen2026-10-26 06:09 UTC38
    fn len(&self) -> usize {
aa3dce5114d0atagen2026-10-26 06:09 UTC39
        self.0
aa3dce5114d0atagen2026-10-26 06:09 UTC40
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC41
}
aa3dce5114d0atagen2026-10-26 06:09 UTC42
aa3dce5114d0atagen2026-10-26 06:09 UTC43
fn expand(salt: &[u8], secret: &[u8], info: &[&[u8]], out: &mut [u8]) -> Result<(), EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC44
    Salt::new(HKDF_SHA256, salt)
aa3dce5114d0atagen2026-10-26 06:09 UTC45
        .extract(secret)
aa3dce5114d0atagen2026-10-26 06:09 UTC46
        .expand(info, Len(out.len()))
aa3dce5114d0atagen2026-10-26 06:09 UTC47
        .and_then(|okm| okm.fill(out))
aa3dce5114d0atagen2026-10-26 06:09 UTC48
        .map_err(|_| EceError::Crypto)
aa3dce5114d0atagen2026-10-26 06:09 UTC49
}
aa3dce5114d0atagen2026-10-26 06:09 UTC50
aa3dce5114d0atagen2026-10-26 06:09 UTC51
struct Keys {
aa3dce5114d0atagen2026-10-26 06:09 UTC52
    cek: [u8; 16],
aa3dce5114d0atagen2026-10-26 06:09 UTC53
    nonce: [u8; 12],
aa3dce5114d0atagen2026-10-26 06:09 UTC54
}
aa3dce5114d0atagen2026-10-26 06:09 UTC55
aa3dce5114d0atagen2026-10-26 06:09 UTC56
fn derive(
aa3dce5114d0atagen2026-10-26 06:09 UTC57
    ecdh_secret: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC58
    auth: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC59
    ua_public: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC60
    as_public: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC61
    salt: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC62
) -> Result<Keys, EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC63
    let mut ikm = [0u8; 32];
aa3dce5114d0atagen2026-10-26 06:09 UTC64
    expand(
aa3dce5114d0atagen2026-10-26 06:09 UTC65
        auth,
aa3dce5114d0atagen2026-10-26 06:09 UTC66
        ecdh_secret,
aa3dce5114d0atagen2026-10-26 06:09 UTC67
        &[b"WebPush: info\0", ua_public, as_public],
aa3dce5114d0atagen2026-10-26 06:09 UTC68
        &mut ikm,
aa3dce5114d0atagen2026-10-26 06:09 UTC69
    )?;
aa3dce5114d0atagen2026-10-26 06:09 UTC70
    let mut keys = Keys {
aa3dce5114d0atagen2026-10-26 06:09 UTC71
        cek: [0; 16],
aa3dce5114d0atagen2026-10-26 06:09 UTC72
        nonce: [0; 12],
aa3dce5114d0atagen2026-10-26 06:09 UTC73
    };
aa3dce5114d0atagen2026-10-26 06:09 UTC74
    expand(
aa3dce5114d0atagen2026-10-26 06:09 UTC75
        salt,
aa3dce5114d0atagen2026-10-26 06:09 UTC76
        &ikm,
aa3dce5114d0atagen2026-10-26 06:09 UTC77
        &[b"Content-Encoding: aes128gcm\0"],
aa3dce5114d0atagen2026-10-26 06:09 UTC78
        &mut keys.cek,
aa3dce5114d0atagen2026-10-26 06:09 UTC79
    )?;
aa3dce5114d0atagen2026-10-26 06:09 UTC80
    expand(salt, &ikm, &[b"Content-Encoding: nonce\0"], &mut keys.nonce)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC81
    Ok(keys)
aa3dce5114d0atagen2026-10-26 06:09 UTC82
}
aa3dce5114d0atagen2026-10-26 06:09 UTC83
aa3dce5114d0atagen2026-10-26 06:09 UTC84
fn ecdh(private: &PrivateKey, public: &[u8]) -> Result<Vec<u8>, EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC85
    agreement::agree(
aa3dce5114d0atagen2026-10-26 06:09 UTC86
        private,
aa3dce5114d0atagen2026-10-26 06:09 UTC87
        UnparsedPublicKey::new(&ECDH_P256, public),
aa3dce5114d0atagen2026-10-26 06:09 UTC88
        EceError::BadKey,
aa3dce5114d0atagen2026-10-26 06:09 UTC89
        |secret| Ok(secret.to_vec()),
aa3dce5114d0atagen2026-10-26 06:09 UTC90
    )
aa3dce5114d0atagen2026-10-26 06:09 UTC91
}
aa3dce5114d0atagen2026-10-26 06:09 UTC92
aa3dce5114d0atagen2026-10-26 06:09 UTC93
/// Encrypts `plaintext` for the subscription whose `p256dh` key is
aa3dce5114d0atagen2026-10-26 06:09 UTC94
/// `ua_public` (uncompressed, 65 bytes) and whose auth secret is `auth`.
aa3dce5114d0atagen2026-10-26 06:09 UTC95
pub fn encrypt(ua_public: &[u8], auth: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC96
    let as_private = PrivateKey::generate(&ECDH_P256).map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC97
    let mut salt = [0u8; 16];
aa3dce5114d0atagen2026-10-26 06:09 UTC98
    aws_lc_rs::rand::fill(&mut salt).map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC99
    encrypt_with(&as_private, salt, ua_public, auth, plaintext)
aa3dce5114d0atagen2026-10-26 06:09 UTC100
}
aa3dce5114d0atagen2026-10-26 06:09 UTC101
aa3dce5114d0atagen2026-10-26 06:09 UTC102
fn encrypt_with(
aa3dce5114d0atagen2026-10-26 06:09 UTC103
    as_private: &PrivateKey,
aa3dce5114d0atagen2026-10-26 06:09 UTC104
    salt: [u8; 16],
aa3dce5114d0atagen2026-10-26 06:09 UTC105
    ua_public: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC106
    auth: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC107
    plaintext: &[u8],
aa3dce5114d0atagen2026-10-26 06:09 UTC108
) -> Result<Vec<u8>, EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC109
    if ua_public.len() != 65 || auth.len() != 16 {
aa3dce5114d0atagen2026-10-26 06:09 UTC110
        return Err(EceError::BadKey);
aa3dce5114d0atagen2026-10-26 06:09 UTC111
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC112
    if plaintext.len() > MAX_PLAINTEXT {
aa3dce5114d0atagen2026-10-26 06:09 UTC113
        return Err(EceError::TooLong);
aa3dce5114d0atagen2026-10-26 06:09 UTC114
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC115
    let as_public = as_private
aa3dce5114d0atagen2026-10-26 06:09 UTC116
        .compute_public_key()
aa3dce5114d0atagen2026-10-26 06:09 UTC117
        .map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC118
    let as_public = as_public.as_ref();
aa3dce5114d0atagen2026-10-26 06:09 UTC119
    let secret = ecdh(as_private, ua_public)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC120
    let keys = derive(&secret, auth, ua_public, as_public, &salt)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC121
aa3dce5114d0atagen2026-10-26 06:09 UTC122
    let mut out = Vec::with_capacity(HEADER_LEN + plaintext.len() + 1 + TAG_LEN);
aa3dce5114d0atagen2026-10-26 06:09 UTC123
    out.extend_from_slice(&salt);
aa3dce5114d0atagen2026-10-26 06:09 UTC124
    out.extend_from_slice(&RECORD_SIZE.to_be_bytes());
aa3dce5114d0atagen2026-10-26 06:09 UTC125
    out.push(as_public.len() as u8);
aa3dce5114d0atagen2026-10-26 06:09 UTC126
    out.extend_from_slice(as_public);
aa3dce5114d0atagen2026-10-26 06:09 UTC127
    let mut record = Vec::with_capacity(plaintext.len() + 1 + TAG_LEN);
aa3dce5114d0atagen2026-10-26 06:09 UTC128
    record.extend_from_slice(plaintext);
aa3dce5114d0atagen2026-10-26 06:09 UTC129
    record.push(2);
aa3dce5114d0atagen2026-10-26 06:09 UTC130
    LessSafeKey::new(UnboundKey::new(&AES_128_GCM, &keys.cek).map_err(|_| EceError::Crypto)?)
aa3dce5114d0atagen2026-10-26 06:09 UTC131
        .seal_in_place_append_tag(
aa3dce5114d0atagen2026-10-26 06:09 UTC132
            Nonce::assume_unique_for_key(keys.nonce),
aa3dce5114d0atagen2026-10-26 06:09 UTC133
            Aad::empty(),
aa3dce5114d0atagen2026-10-26 06:09 UTC134
            &mut record,
aa3dce5114d0atagen2026-10-26 06:09 UTC135
        )
aa3dce5114d0atagen2026-10-26 06:09 UTC136
        .map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC137
    out.extend_from_slice(&record);
aa3dce5114d0atagen2026-10-26 06:09 UTC138
    Ok(out)
aa3dce5114d0atagen2026-10-26 06:09 UTC139
}
aa3dce5114d0atagen2026-10-26 06:09 UTC140
aa3dce5114d0atagen2026-10-26 06:09 UTC141
/// The user agent's side, for tests and the mock push service.
aa3dce5114d0atagen2026-10-26 06:09 UTC142
pub fn decrypt(ua_private: &PrivateKey, auth: &[u8], body: &[u8]) -> Result<Vec<u8>, EceError> {
aa3dce5114d0atagen2026-10-26 06:09 UTC143
    if body.len() < 21 {
aa3dce5114d0atagen2026-10-26 06:09 UTC144
        return Err(EceError::Crypto);
aa3dce5114d0atagen2026-10-26 06:09 UTC145
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC146
    let salt = &body[..16];
aa3dce5114d0atagen2026-10-26 06:09 UTC147
    let id_len = body[20] as usize;
aa3dce5114d0atagen2026-10-26 06:09 UTC148
    let as_public = body.get(21..21 + id_len).ok_or(EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC149
    let mut record = body[21 + id_len..].to_vec();
aa3dce5114d0atagen2026-10-26 06:09 UTC150
    let ua_public = ua_private
aa3dce5114d0atagen2026-10-26 06:09 UTC151
        .compute_public_key()
aa3dce5114d0atagen2026-10-26 06:09 UTC152
        .map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC153
    let secret = ecdh(ua_private, as_public)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC154
    let keys = derive(&secret, auth, ua_public.as_ref(), as_public, salt)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC155
    let plain =
aa3dce5114d0atagen2026-10-26 06:09 UTC156
        LessSafeKey::new(UnboundKey::new(&AES_128_GCM, &keys.cek).map_err(|_| EceError::Crypto)?)
aa3dce5114d0atagen2026-10-26 06:09 UTC157
            .open_in_place(
aa3dce5114d0atagen2026-10-26 06:09 UTC158
                Nonce::assume_unique_for_key(keys.nonce),
aa3dce5114d0atagen2026-10-26 06:09 UTC159
                Aad::empty(),
aa3dce5114d0atagen2026-10-26 06:09 UTC160
                &mut record,
aa3dce5114d0atagen2026-10-26 06:09 UTC161
            )
aa3dce5114d0atagen2026-10-26 06:09 UTC162
            .map_err(|_| EceError::Crypto)?;
aa3dce5114d0atagen2026-10-26 06:09 UTC163
    match plain.iter().rposition(|&byte| byte != 0) {
aa3dce5114d0atagen2026-10-26 06:09 UTC164
        Some(end) if plain[end] == 2 => Ok(plain[..end].to_vec()),
aa3dce5114d0atagen2026-10-26 06:09 UTC165
        _ => Err(EceError::Crypto),
aa3dce5114d0atagen2026-10-26 06:09 UTC166
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC167
}
aa3dce5114d0atagen2026-10-26 06:09 UTC168
aa3dce5114d0atagen2026-10-26 06:09 UTC169
#[cfg(test)]
aa3dce5114d0atagen2026-10-26 06:09 UTC170
mod tests {
aa3dce5114d0atagen2026-10-26 06:09 UTC171
    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
aa3dce5114d0atagen2026-10-26 06:09 UTC172
aa3dce5114d0atagen2026-10-26 06:09 UTC173
    use super::*;
aa3dce5114d0atagen2026-10-26 06:09 UTC174
aa3dce5114d0atagen2026-10-26 06:09 UTC175
    fn b64(value: &str) -> Vec<u8> {
aa3dce5114d0atagen2026-10-26 06:09 UTC176
        URL_SAFE_NO_PAD.decode(value).unwrap()
aa3dce5114d0atagen2026-10-26 06:09 UTC177
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC178
aa3dce5114d0atagen2026-10-26 06:09 UTC179
    // RFC 8291 section 5 and appendix A
aa3dce5114d0atagen2026-10-26 06:09 UTC180
    const PLAINTEXT: &str = "V2hlbiBJIGdyb3cgdXAsIEkgd2FudCB0byBiZSBhIHdhdGVybWVsb24";
aa3dce5114d0atagen2026-10-26 06:09 UTC181
    const AS_PRIVATE: &str = "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw";
aa3dce5114d0atagen2026-10-26 06:09 UTC182
    const UA_PUBLIC: &str =
aa3dce5114d0atagen2026-10-26 06:09 UTC183
        "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4";
aa3dce5114d0atagen2026-10-26 06:09 UTC184
    const UA_PRIVATE: &str = "q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94";
aa3dce5114d0atagen2026-10-26 06:09 UTC185
    const SALT: &str = "DGv6ra1nlYgDCS1FRnbzlw";
aa3dce5114d0atagen2026-10-26 06:09 UTC186
    const AUTH: &str = "BTBZMqHH6r4Tts7J_aSIgg";
aa3dce5114d0atagen2026-10-26 06:09 UTC187
    const MESSAGE: &str = "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN";
aa3dce5114d0atagen2026-10-26 06:09 UTC188
aa3dce5114d0atagen2026-10-26 06:09 UTC189
    #[test]
aa3dce5114d0atagen2026-10-26 06:09 UTC190
    fn rfc8291_vector_encrypts_exactly() {
aa3dce5114d0atagen2026-10-26 06:09 UTC191
        let as_private = PrivateKey::from_private_key(&ECDH_P256, &b64(AS_PRIVATE)).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC192
        let salt: [u8; 16] = b64(SALT).try_into().unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC193
        let body = encrypt_with(
aa3dce5114d0atagen2026-10-26 06:09 UTC194
            &as_private,
aa3dce5114d0atagen2026-10-26 06:09 UTC195
            salt,
aa3dce5114d0atagen2026-10-26 06:09 UTC196
            &b64(UA_PUBLIC),
aa3dce5114d0atagen2026-10-26 06:09 UTC197
            &b64(AUTH),
aa3dce5114d0atagen2026-10-26 06:09 UTC198
            &b64(PLAINTEXT),
aa3dce5114d0atagen2026-10-26 06:09 UTC199
        )
aa3dce5114d0atagen2026-10-26 06:09 UTC200
        .unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC201
        assert_eq!(URL_SAFE_NO_PAD.encode(body), MESSAGE);
aa3dce5114d0atagen2026-10-26 06:09 UTC202
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC203
aa3dce5114d0atagen2026-10-26 06:09 UTC204
    #[test]
aa3dce5114d0atagen2026-10-26 06:09 UTC205
    fn rfc8291_vector_decrypts() {
aa3dce5114d0atagen2026-10-26 06:09 UTC206
        let ua_private = PrivateKey::from_private_key(&ECDH_P256, &b64(UA_PRIVATE)).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC207
        let plain = decrypt(&ua_private, &b64(AUTH), &b64(MESSAGE)).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC208
        assert_eq!(plain, b64(PLAINTEXT));
aa3dce5114d0atagen2026-10-26 06:09 UTC209
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC210
aa3dce5114d0atagen2026-10-26 06:09 UTC211
    #[test]
aa3dce5114d0atagen2026-10-26 06:09 UTC212
    fn random_keys_round_trip_and_bad_input_is_refused() {
aa3dce5114d0atagen2026-10-26 06:09 UTC213
        let ua_private = PrivateKey::generate(&ECDH_P256).unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC214
        let ua_public = ua_private.compute_public_key().unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC215
        let auth = [9u8; 16];
aa3dce5114d0atagen2026-10-26 06:09 UTC216
        let body = encrypt(ua_public.as_ref(), &auth, b"hello").unwrap();
aa3dce5114d0atagen2026-10-26 06:09 UTC217
        assert_eq!(decrypt(&ua_private, &auth, &body).unwrap(), b"hello");
aa3dce5114d0atagen2026-10-26 06:09 UTC218
        assert!(decrypt(&ua_private, &[1u8; 16], &body).is_err());
aa3dce5114d0atagen2026-10-26 06:09 UTC219
        assert_eq!(encrypt(&[4u8; 65], &auth, b"x"), Err(EceError::BadKey));
aa3dce5114d0atagen2026-10-26 06:09 UTC220
        assert_eq!(
aa3dce5114d0atagen2026-10-26 06:09 UTC221
            encrypt(ua_public.as_ref(), &auth, &vec![0; MAX_PLAINTEXT + 1]),
aa3dce5114d0atagen2026-10-26 06:09 UTC222
            Err(EceError::TooLong)
aa3dce5114d0atagen2026-10-26 06:09 UTC223
        );
aa3dce5114d0atagen2026-10-26 06:09 UTC224
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC225
}