diff --git a/Cargo.lock b/Cargo.lock index bcd1dd4..1f91919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,16 +30,19 @@ dependencies = [ "clap", "criterion", "data-encoding", + "ecdsa", "fjall", "futures", "governor", "http-body-util", + "k256", "log", "multibase", "native-tls", "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", + "p256", "poem", "postgres-native-tls", "reqwest", @@ -49,6 +52,7 @@ dependencies = [ "rustls", "serde", "serde_bytes", + "serde_ipld_dagcbor", "serde_json", "tempfile", "thiserror 2.0.18", @@ -265,6 +269,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base256emoji" version = "1.0.2" @@ -281,6 +291,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "1.3.3" @@ -368,6 +384,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbor4ii" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544cf8c89359205f4f990d0e6f3828db42df85b5dac95d09157a250eb0749c4" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.2.56" @@ -442,6 +467,8 @@ dependencies = [ "core2", "multibase", "multihash", + "serde", + "serde_bytes", "unsigned-varint", ] @@ -526,6 +553,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-str" version = "0.4.3" @@ -661,6 +694,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -711,6 +756,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -741,6 +797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -762,12 +819,46 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -817,6 +908,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1009,6 +1110,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1074,6 +1176,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.13" @@ -1460,6 +1573,17 @@ dependencies = [ "compare", ] +[[package]] +name = "ipld-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090f624976d72f0b0bb71b86d58dc16c15e069193067cb3a3a09d655246cbbda" +dependencies = [ + "cid", + "serde", + "serde_bytes", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1526,6 +1650,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1706,6 +1844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", + "serde", "unsigned-varint", ] @@ -1960,6 +2099,18 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "page_size" version = "0.6.0" @@ -2028,6 +2179,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2085,6 +2245,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2257,6 +2427,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2634,6 +2813,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rfc7239" version = "0.1.3" @@ -2799,6 +2988,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2874,6 +3077,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_ipld_dagcbor" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46182f4f08349a02b45c998ba3215d3f9de826246ba02bb9dddfe9a2a2100778" +dependencies = [ + "cbor4ii", + "ipld-core", + "scopeguard", + "serde", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -2957,6 +3172,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -3009,6 +3234,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 8f9580a..caf185c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,4 +49,8 @@ rmp-serde = "1.3.1" bincode = "1.3.3" serde_bytes = "0.11.19" multibase = "0.9.2" +ecdsa = "0.16.9" +p256 = "0.13.2" +k256 = "0.13.4" +serde_ipld_dagcbor = "0.6.4" diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..25bc1ab --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,219 @@ +use data_encoding::BASE64URL_NOPAD; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// base64url-encoded ECDSA signature → raw bytes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Signature(#[serde(with = "serde_bytes")] pub Vec); + +impl Signature { + pub fn from_base64url(s: &str) -> anyhow::Result { + BASE64URL_NOPAD + .decode(s.as_bytes()) + .map(Self) + .map_err(|e| anyhow::anyhow!("invalid base64url sig {s}: {e}")) + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&BASE64URL_NOPAD.encode(&self.0)) + } +} + +/// did:key:z... → raw multicodec public key bytes +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DidKey(#[serde(with = "serde_bytes")] pub Vec); + +impl DidKey { + pub fn from_did_key(s: &str) -> anyhow::Result { + let multibase_str = s + .strip_prefix("did:key:") + .ok_or_else(|| anyhow::anyhow!("missing did:key: prefix in {s}"))?; + let (_base, bytes) = multibase::decode(multibase_str) + .map_err(|e| anyhow::anyhow!("invalid multibase in did:key {s}: {e}"))?; + Ok(Self(bytes)) + } +} + +impl fmt::Display for DidKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "did:key:{}", + multibase::encode(multibase::Base::Base58Btc, &self.0) + ) + } +} + +const P256_PREFIX: [u8; 2] = [0x80, 0x24]; +const K256_PREFIX: [u8; 2] = [0xe7, 0x01]; + +/// verifies a plc op signature +/// +/// - `key` : did:key:z... public key +/// - `data`: dag-cbor encoded op without the `sig` field (sha256 is applied internally) +/// - `sig` : signature bytes decoded from the base64url `sig` field of the op +pub fn verify_plc_sig(key: &DidKey, data: &[u8], sig: &Signature) -> anyhow::Result<()> { + use ecdsa::signature::Verifier as _; + + let prefix: [u8; 2] = key + .0 + .get(..2) + .ok_or_else(|| anyhow::anyhow!("key bytes too short: {key}"))? + .try_into() + .map_err(|_| anyhow::anyhow!("key bytes too short: {key}"))?; + let pubkey = key + .0 + .get(2..) + .ok_or_else(|| anyhow::anyhow!("key bytes too short: {key}"))?; + + match prefix { + P256_PREFIX => { + use p256::ecdsa::{Signature, VerifyingKey}; + + let key = VerifyingKey::from_sec1_bytes(pubkey) + .map_err(|e| anyhow::anyhow!("bad p256 key {pubkey:?}: {e}"))?; + let sig = Signature::from_slice(&sig.0) + .map_err(|e| anyhow::anyhow!("bad p256 sig {sig}: {e}"))?; + if sig.normalize_s().is_some() { + anyhow::bail!("high-S signature is not allowed for plc"); + } + key.verify(data, &sig) + .map_err(|e| anyhow::anyhow!("invalid p256 signature {sig}: {e}")) + } + K256_PREFIX => { + use k256::ecdsa::{Signature, VerifyingKey}; + + let key = VerifyingKey::from_sec1_bytes(pubkey) + .map_err(|e| anyhow::anyhow!("bad k256 key {pubkey:?}: {e}"))?; + let sig = Signature::from_slice(&sig.0) + .map_err(|e| anyhow::anyhow!("bad k256 sig {sig}: {e}"))?; + if sig.normalize_s().is_some() { + anyhow::bail!("high-S signature is not allowed for plc"); + } + key.verify(data, &sig) + .map_err(|e| anyhow::anyhow!("invalid k256 signature {sig}: {e}")) + } + _ => anyhow::bail!("unsupported key prefix: {:02x?}", prefix), + } +} + +pub struct AssuranceResults { + pub valid: bool, + pub errors: Vec, +} + +/// assures that an op has a valid signature +/// +/// - `keys`: the rotation keys from the previous operation (or it's own keys if genesis op) +/// - `sig` : the signature to check. +/// - `data`: the operation to check, without the sig field. +pub fn assure_valid_sig<'key>( + keys: impl IntoIterator, + sig: &Signature, + data: &serde_json::Value, +) -> anyhow::Result { + let serde_json::Value::Object(data) = data else { + anyhow::bail!("invalid op, not an object"); + }; + if data.contains_key("sig") { + anyhow::bail!("data should not include the sig"); + } + let data = serde_ipld_dagcbor::to_vec(&data)?; + let mut results = AssuranceResults { + valid: false, + errors: Vec::new(), + }; + for key in keys { + match verify_plc_sig(key, &data, sig) { + Ok(_) => { + results.valid = true; + break; + } + Err(e) => results.errors.push(e), + } + } + Ok(results) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn signature_roundtrip() { + let original = "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA"; + let sig = Signature::from_base64url(original).unwrap(); + assert_eq!(sig.0.len(), 64); + assert_eq!(sig.to_string(), original); + } + + #[test] + fn did_key_roundtrip() { + let original = "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg"; + let key = DidKey::from_did_key(original).unwrap(); + assert_eq!(key.to_string(), original); + } + + #[test] + fn test_fixture_signatures() { + let fixtures = [ + "tests/fixtures/log_bskyapp.json", + "tests/fixtures/log_legacy_dholms.json", + "tests/fixtures/log_nullification.json", + "tests/fixtures/log_tombstone.json", + ]; + + for path in fixtures { + let data = std::fs::read_to_string(path).unwrap(); + let entries: Vec = serde_json::from_str(&data).unwrap(); + + let mut ops_by_cid: HashMap = HashMap::new(); + + for entry in entries { + let mut data = entry["operation"].clone(); + let cid = entry["cid"].as_str().unwrap().to_string(); + + let sig_str = data["sig"].as_str().unwrap(); + let sig = Signature::from_base64url(sig_str).unwrap(); + + data.as_object_mut().unwrap().remove("sig"); + + let prev_cid = data["prev"].as_str().unwrap_or(""); + let op = ops_by_cid.get(prev_cid).unwrap_or(&data); + + let mut valid_keys = Vec::new(); + if let Some(arr) = op["rotationKeys"].as_array() { + for k in arr { + valid_keys.push(DidKey::from_did_key(k.as_str().unwrap()).unwrap()); + } + } + if let Some(rk) = op["recoveryKey"].as_str() { + valid_keys.push(DidKey::from_did_key(rk).unwrap()); + } + if let Some(sk) = op["signingKey"].as_str() { + valid_keys.push(DidKey::from_did_key(sk).unwrap()); + } + + assert!( + !valid_keys.is_empty(), + "no keys to verify against for {}", + path + ); + + let results = assure_valid_sig(&valid_keys, &sig, &data) + .expect("that we used the function correctly"); + for err in results.errors { + println!("{path}/{cid}: {err}"); + } + if !results.valid { + panic!("signature verification failed in {path}/{cid}"); + } + + ops_by_cid.insert(cid, data); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f481635..29f5bc6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use tokio::sync::{mpsc, oneshot}; mod backfill; mod cached_value; mod client; +mod crypto; pub mod doc; mod mirror; mod plc_fjall; diff --git a/src/plc_fjall.rs b/src/plc_fjall.rs index 8c2f18f..8a667d4 100644 --- a/src/plc_fjall.rs +++ b/src/plc_fjall.rs @@ -1,7 +1,9 @@ -use crate::{BundleSource, Week}; -use crate::{Dt, ExportPage, Op as CommonOp, PageBoundaryState}; +use crate::{ + BundleSource, Dt, ExportPage, Op as CommonOp, PageBoundaryState, Week, + crypto::{DidKey, Signature}, +}; use anyhow::Context; -use data_encoding::{BASE32_NOPAD, BASE64URL_NOPAD}; +use data_encoding::BASE32_NOPAD; use fjall::{ Database, Keyspace, KeyspaceCreateOptions, OwnedWriteBatch, PersistMode, config::BlockSizePolicy, @@ -89,50 +91,6 @@ fn decode_timestamp(key: &[u8]) -> anyhow::Result
{ .ok_or_else(|| anyhow::anyhow!("invalid timestamp {micros}")) } -/// base64url-encoded ECDSA signature → raw bytes -#[derive(Debug, Clone, Serialize, Deserialize)] -struct Signature(#[serde(with = "serde_bytes")] Vec); - -impl Signature { - fn from_base64url(s: &str) -> anyhow::Result { - BASE64URL_NOPAD - .decode(s.as_bytes()) - .map(Self) - .map_err(|e| anyhow::anyhow!("invalid base64url sig {s}: {e}")) - } -} - -impl fmt::Display for Signature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&BASE64URL_NOPAD.encode(&self.0)) - } -} - -/// did:key:z... → raw multicodec public key bytes -#[derive(Debug, Clone, Serialize, Deserialize)] -struct DidKey(#[serde(with = "serde_bytes")] Vec); - -impl DidKey { - fn from_did_key(s: &str) -> anyhow::Result { - let multibase_str = s - .strip_prefix("did:key:") - .ok_or_else(|| anyhow::anyhow!("missing did:key: prefix in {s}"))?; - let (_base, bytes) = multibase::decode(multibase_str) - .map_err(|e| anyhow::anyhow!("invalid multibase in did:key {s}: {e}"))?; - Ok(Self(bytes)) - } -} - -impl fmt::Display for DidKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "did:key:{}", - multibase::encode(multibase::Base::Base58Btc, &self.0) - ) - } -} - /// CID string → binary CID bytes #[derive(Debug, Clone, Serialize, Deserialize)] struct PlcCid(#[serde(with = "serde_bytes")] Vec); @@ -1214,21 +1172,6 @@ pub async fn pages_to_fjall( mod tests { use super::*; - #[test] - fn signature_roundtrip() { - let original = "9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA"; - let sig = Signature::from_base64url(original).unwrap(); - assert_eq!(sig.0.len(), 64); - assert_eq!(sig.to_string(), original); - } - - #[test] - fn did_key_roundtrip() { - let original = "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg"; - let key = DidKey::from_did_key(original).unwrap(); - assert_eq!(key.to_string(), original); - } - #[test] fn plc_cid_roundtrip() { let original = "bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm";