From b4db5a8667a5fe150adad68c9fdce51d039cd95b Mon Sep 17 00:00:00 2001 From: bain Date: Sat, 1 Jun 2024 22:44:32 +0200 Subject: [PATCH] refactor, rethink, rewrite --- Cargo.lock | 114 ++-- Cargo.toml | 3 +- README.md | 25 +- src/accounts.rs | 165 ++++++ src/authorization.html | 289 ++++++--- src/homepage.html | 127 ++++ src/main.rs | 556 ++++++++++-------- src/names.rs | 24 - static/fonts.css | 39 ++ .../poppins-v21-latin_latin-ext-700.woff2 | Bin 0 -> 10736 bytes ...oppins-v21-latin_latin-ext-700italic.woff2 | Bin 0 -> 11692 bytes .../poppins-v21-latin_latin-ext-italic.woff2 | Bin 0 -> 11788 bytes .../poppins-v21-latin_latin-ext-regular.woff2 | Bin 0 -> 10832 bytes 13 files changed, 939 insertions(+), 403 deletions(-) create mode 100644 src/accounts.rs create mode 100644 src/homepage.html delete mode 100644 src/names.rs create mode 100644 static/fonts.css create mode 100644 static/fonts/poppins-v21-latin_latin-ext-700.woff2 create mode 100644 static/fonts/poppins-v21-latin_latin-ext-700italic.woff2 create mode 100644 static/fonts/poppins-v21-latin_latin-ext-italic.woff2 create mode 100644 static/fonts/poppins-v21-latin_latin-ext-regular.woff2 diff --git a/Cargo.lock b/Cargo.lock index e230e29..5c491ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,8 +82,9 @@ dependencies = [ "ring", "serde", "serde_json", + "serde_yaml", "tide", - "toml", + "tide-serve-dir-macro", ] [[package]] @@ -1382,6 +1383,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "semver" version = "0.9.0" @@ -1448,15 +1458,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "serde_spanned" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1469,6 +1470,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.6.1" @@ -1738,6 +1752,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tide-serve-dir-macro" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0569876d14d685e8047bb24104c2e5c36768507cb0e3bc719b962c10d08c6e8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "walkdir", +] + [[package]] name = "time" version = "0.2.27" @@ -1791,40 +1817,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "toml" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tracing" version = "0.1.40" @@ -1878,6 +1870,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -1944,6 +1942,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -2050,6 +2058,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2196,12 +2213,3 @@ name = "windows_x86_64_msvc" version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] diff --git a/Cargo.toml b/Cargo.toml index 1ef894f..fc5de3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,9 @@ async-std = { version = "1.8.0", features = ["attributes"] } tide = { version = "0.16.0", features = [] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.9.34" ring = { version = "0.17.8", features = ["std"] } anyhow = "1.0.70" base64 = "0.21.0" -toml = "0.7.3" hex = "0.4.3" +tide-serve-dir-macro = "0.1" diff --git a/README.md b/README.md index 540079e..45168b2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# ^NON +# ^NON \[anon\] -Extremely rudimentary OIDC provider. Users hold account numbers from which +Extremely rudimentary OIDC provider. Users hold account codes from which their identities are derived on-demand. Each identity is separate for different services, but can be accessed from a -single account number. ^NON does not have a database of the users, so nobody +single account code. ^NON does not have a database of the users, so nobody can correlate user information across services. ## Installation @@ -14,4 +14,21 @@ can correlate user information across services. 2. fill out `config.toml.sample`. The server expects a file called `config.toml` in its working directory. -3. Enjoy :) +3. Generate the keypair for signing JWT tokens with: + ```bash + openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:65537 | \ + openssl pkcs8 -topk8 -nocrypt -outform der > rsa-key.pk8 + ``` + +4. Enjoy :) + +## Deployment notes + +When deploying, you should be aware of the potential of a birthday attack on +the system. For `v1` of the account code, we should expect a collision after +about `2^36` unique accounts, which means that, without rate-limiting, there is +the potential to brute-force an account / accidentally log into someone else's +account. You should consider the amount of users which will use the system, and +set up a rate-limiter. + +Improbable things happen all the time, so better safe than sorry :) diff --git a/src/accounts.rs b/src/accounts.rs new file mode 100644 index 0000000..1c66307 --- /dev/null +++ b/src/accounts.rs @@ -0,0 +1,165 @@ +/// v1 of the account identificators +use std::time::{SystemTime, UNIX_EPOCH}; + +use ring::rand::{SecureRandom, SystemRandom}; +use serde_json::{json, Value}; + +const ADJECTIVES: &[&'static str] = &[ + "Affinity", "Artisan", "Benevolent", "Breezy", "Cheerful", "Compass", + "Curious", "Eager", "Ebullient", "Effervescent", "Esteem", "Evergreen", + "Flourish", "Genuine", "Grounded", "Harmony", "Inventive", "Journey", + "Jubilant", "Kaleidoscope", "Kaleidoscopic", "Kindred", "Marvel", "Melodious", + "Optimistic", "Resilient", "Riverstone", "Serendipitous", "Serendipity", "Serene", + "Sincere", "Skybound", "Sparkling", "Sprightly", "Stardust", "Stargazer", + "Starry", "Steadfast", "Sunbeam", "Tapestry", "Tenacious", "Upbeat", + "Valiant", "Verdant", "Vigorous", "Wanderlust", "Whimsical", "Witty", + "Wonderstruck", "Zephyr", +]; + +const ANIMALS: &[&'static str] = &[ + "Axolotl", "Badger", "Baku", "Butterfly", "Capybara", "Carbuncle", + "Chameleon", "Cheetah", "Deer", "Dolphin", "Dryad", "Echidna", + "Elephant", "Fennec Fox", "Flamingo", "Fox", "Fu", "Gnome", + "Hippocampus", "Hippogriff", "Hippopotamus", "Hummingbird", "Iguana", "Kangaroo", + "Kelpie", "Kirin", "Koala", "Dragon", "Leviathan", "Lion", + "Lynx", "Manatee", "Manok", "Meerkat", "Mooncalf", "Naiad", + "Narwhal", "Orangutan", "Owl", "Panda", "Parrot", "Peacock", + "Penguin", "Phoenix", "Pixie", "Qilin", "Quokka", "Raccoon", + "Otter", "Seahorse", "Serpent", "Simurgh", "Sphynx", "Sprite", + "Squirrel", "Sylph", "Tapir", "Toucan", "Turtle", "Unicorn", + "Whale", "Ziz", +]; + +fn checksum(ns: &[u8]) -> u8 { + (ns.iter().enumerate().fold(0, |acc, b| { + if b.0 % 2 == 0 { + acc + *b.1 as i32 * 2 + } else { + acc + *b.1 as i32 + } + }) % 36) as u8 +} + +pub fn normalize(account: &str) -> Option { + // we interpret the account "number" in ASCII + let account_low = account.to_lowercase(); + + if account_low.chars().next() != Some('a') { + return None; + } + + let ns: Vec = account_low + .bytes() + .filter_map(|b| { + // base-36 + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'z' => Some(b - b'a' + 10), + _ => None, + } + }) + .collect(); + + if ns.len() != 16 { + return None; + } + + // Calculate the checksum (modified Luhn's algorithm) + let checksum = checksum(&ns[..ns.len() - 1]); + + if *ns.last().unwrap() != checksum { + return None; + } + + Some(account_low) +} + +pub fn generate_account() -> anyhow::Result { + let mut code = Vec::with_capacity(16); + code.push(10); // 'a' + + let rng = SystemRandom::new(); + 'outer: loop { + let mut bytes = [0u8; 14]; + rng.fill(&mut bytes)?; + + for byte in bytes { + if byte > u8::MAX - u8::MAX % 36 { + continue; + } + + code.push(byte % 36); + + if code.len() == 15 { + break 'outer; + } + } + } + + code.push(checksum(&code)); + + Ok(String::from_utf8( + code.iter() + .map(|n| match n { + 0..=9 => b'0' + n, + 10..=35 => b'a' + n - 10, + _ => unreachable!(), + }) + .collect(), + )? + .to_uppercase()) +} + +pub fn create_id_token_claims( + salt: &str, + issuer_uri: &str, + client_id: &str, + normalized_account: &str, + nonce: Option<&str>, +) -> Value { + // we can expect that + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("no time travelling allowed before 1970") + .as_secs(); + + use ring::hmac; + let mangler = hmac::Key::new( + hmac::HMAC_SHA256, + (salt.to_owned() + &normalized_account).as_bytes(), + ); + + let mangle = |what: &str| { + let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes()); + hex::encode(tag.as_ref()) + }; + let choose = |what: &str, from: &[&'static str]| { + let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes()); + let mut bytes: [u8; 4] = [0; 4]; + bytes.copy_from_slice(tag.as_ref()); + from[i32::from_le_bytes(bytes) as usize % from.len()] + }; + + let adjective = choose("family_name", ADJECTIVES); + let animal = choose("given_name", ANIMALS); + + let mut body = json!({ + "kid": "master", + "iss": issuer_uri, + "sub": mangle("sub")[..32], + "aud": client_id, + "exp": now + crate::TOKEN_EXPIRATION, + "iat": now, + "given_name": animal, + "family_name": adjective, + "email": mangle("email")[..24].to_owned()+"@email.invalid", + "email_verified": false, + "preferred_username": adjective.to_owned()+animal, + }); + + if let Some(nonce) = nonce { + body["nonce"] = nonce.into(); + } + + body +} diff --git a/src/authorization.html b/src/authorization.html index 02f120f..41395c3 100644 --- a/src/authorization.html +++ b/src/authorization.html @@ -4,10 +4,10 @@ - + {{issuer_name}}: log into {{client_name}} -
-

^NON : {{issuer_name}}

-

You are logging into

-

Přihlašujete se do

-

{{name}}

-
- - -
-
- -

- or generate a new account number - -

+
+ +
+

^NON : {{issuer_name}}

+

+ Logging into: + + {{client_name}} +

+ +
+
+ + +
+ +
+ -
-
+

{{notice}}

+ + + diff --git a/src/homepage.html b/src/homepage.html new file mode 100644 index 0000000..41aecfc --- /dev/null +++ b/src/homepage.html @@ -0,0 +1,127 @@ + + + + + + + ^NON at {{issuer_name}} + + + + +
+
+

This is ^NON

+

Toto je ^NON

+

An anonymous identity provider for accessing services from {{issuer_name}}.

+ +
+

No account saved in browser.

+ +

Account saved in browser. Forget?

+ +
+ +
+

How does it work?

+

+ ^NON [anon] creates your identities for different services based on an account code, which you can generate + during login. Each service gets a different identity, so nobody can connect data between them. You, + on the other hand, can connect to all of them using only a single account code. +

+

You can check out the technical details here.

+
+ + +
+
+ + + + diff --git a/src/main.rs b/src/main.rs index 90a95eb..8dd59a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,15 @@ -mod names; +mod accounts; + use async_std::fs; use async_std::fs::File; use async_std::sync::Mutex; use ring::rand::SecureRandom; use ring::rand::SystemRandom; -use serde_json::Value; use std::collections::HashMap; use std::collections::LinkedList; use std::env; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::SystemTime; use std::time::UNIX_EPOCH; @@ -16,7 +18,7 @@ use tide::log; use tide::prelude::json; use tide::Request; use tide::Response; -use toml::Table; +use tide_serve_dir_macro::auto_serve_dir; use anyhow::{anyhow, Result}; use async_std::io::ReadExt; @@ -27,149 +29,38 @@ use std::fmt; use base64::{engine::general_purpose as base64_coder, Engine as _}; +const TOKEN_EXPIRATION: u64 = 600; +const AUTHORIZATION_EXPIRATION: u64 = 60; + #[derive(Deserialize, Clone)] pub struct Client { - pub name: String, - pub client_secret: String, - pub redirect_uris: Vec, + name: String, + client_secret: String, + redirect_uris: Vec, } #[derive(Deserialize)] pub struct Config { - pub host: String, - pub port: u16, - pub clients: Table, - pub issuer_uri: String, - pub issuer_name: String, - pub rsa_key_file: String, - pub salt: String, + host: String, + port: u16, + clients: HashMap, + issuer_uri: String, + issuer_name: String, + rsa_key_file: String, + salt: String, } impl Config { pub async fn from_file(file: &mut File) -> Result { let mut buf = String::new(); file.read_to_string(&mut buf).await?; - match toml::from_str::(&buf) { + match serde_yaml::from_str::(&buf) { Ok(v) => Ok(v), Err(e) => Err(anyhow!("failed to parse config file: {}", e)), } } } -async fn error_handler(mut res: tide::Response) -> tide::Result { - if let Some(err) = res.downcast_error::() { - res.set_body(tide::Body::from_json(err)?); - res.set_status(400); - } - Ok(res) -} - -async fn authorize(req: Request) -> tide::Result { - #[derive(Deserialize)] - struct Query { - // response_type: String, - client_id: String, - // scope: String, // dont care rn - state: String, - redirect_uri: String, - uid: Option, - code_challenge: Option, - code_challenge_method: Option, - nonce: Option, - } - let q: Query = req.query()?; - - let i = req - .state() - .clients - .get(&q.client_id) - .ok_or(OAuthError::new("invalid_client", "Unknown client"))?; - - // return the login page - if q.uid.is_none() { - return Ok(Response::builder(200) - .body( - // I could use a rendering library here, but its literally as simple as replacing - // two strings. - include_str!("authorization.html") - .replace("{{name}}", &i.name) - .replace("{{issuer_name}}", &req.state().issuer_name), - ) - .header("Content-Type", "text/html") - .into()); - } - - let uid = q.uid.unwrap(); - - // check redirect uri validity - if i.redirect_uris.iter().all(|r| r.as_str() != q.redirect_uri) { - return Err(OAuthError::new("invalid_redirect", "").into()); - } - - let random = SystemRandom::new(); - let mut salt = [0u8; 32]; - random.fill(&mut salt)?; - - let code = base64_coder::URL_SAFE_NO_PAD.encode(&salt); - - let redirect = - Url::parse_with_params(&q.redirect_uri, &[("state", &q.state), ("code", &code)])?; - - let pkce = if q.code_challenge.is_some() && q.code_challenge_method.is_some() { - Some(PKCE { - challenge: q.code_challenge.unwrap(), - method: q.code_challenge_method.unwrap(), - }) - } else { - None - }; - - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - - let mut auths = req.state().authorizations.lock().await; - - // clean up old authorizations - while let Some(s) = auths.expirations.front() { - if s.1 > timestamp { - break; - } - let s = auths.expirations.pop_front().unwrap(); - auths.auths.remove(&s.0); - } - - auths.auths.insert( - code.clone(), - Authorization { - client_id: q.client_id, - uid, - redirect_uri: q.redirect_uri, - pkce, - nonce: q.nonce, - exp: timestamp + 60, - }, - ); - auths.expirations.push_back((code.clone(), timestamp + 60)); - - let mut resp = Response::new(302); - resp.insert_header("Location", redirect.as_str()); - - Ok(resp) -} - -fn parse_basic_auth(header: &str) -> Option<(String, String)> { - let v = header.strip_prefix("Basic ")?; - - let parsed = String::from_utf8(base64_coder::STANDARD.decode(v).ok()?).ok()?; - - let parts: Vec<&str> = parsed.split(':').take(2).collect(); - - if parts.len() != 2 { - return None; - } - - Some((parts[0].to_owned(), parts[1].to_owned())) -} - #[derive(Debug, Serialize)] pub struct OAuthError { pub error: String, @@ -193,65 +84,208 @@ impl fmt::Display for OAuthError { impl Error for OAuthError {} -fn create_id_token_claims( - salt: &str, - issuer_uri: &str, - client_id: &str, - uid: &str, +async fn error_handler(res: tide::Response) -> tide::Result { + if let Some(err) = res.downcast_error::() { + return Ok(tide::Response::builder(400) + .body(tide::Body::from_json(err)?) + .build()); + } + Ok(res) +} + +#[derive(Deserialize)] +struct AuthorizationQuery { + response_type: String, + client_id: String, + redirect_uri: String, + state: Option, + code_challenge: Option, + code_challenge_method: Option, nonce: Option, -) -> anyhow::Result { - let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + // scope: String, // we only issue tokens with the "openid email" scope +} - use ring::hmac; - let mangler = hmac::Key::new(hmac::HMAC_SHA256, (salt.to_owned() + uid).as_bytes()); +fn redirect_with_query(redirect_uri: &str, query: &[(&str, Option<&str>)]) -> tide::Result { + let filtered = query.into_iter().filter_map(|x| x.1.map(|v| (x.0, v))); + let redirect = Url::parse_with_params(redirect_uri, filtered)?; + Ok(tide::Redirect::new(redirect).into()) +} - let mangle = |what: &str| { - let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes()); - hex::encode(tag.as_ref()) - }; - let choose = |what: &str, from: &[&'static str]| { - let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes()); - let bytes = tag.as_ref(); - let mut i: usize = 0; - i = (i << 8) + bytes[0] as usize; - i = (i << 8) + bytes[1] as usize; - i = (i << 8) + bytes[2] as usize; - i = (i << 8) + bytes[3] as usize; - from[i % from.len()] - }; +fn render_login_page(client_name: &str, issuer_name: &str, notice: &str) -> tide::Response { + Response::builder(200) + .body( + // I could use a rendering library here, but its literally as simple as replacing + // a few strings from a trusted config. + include_str!("authorization.html") + .replace("{{client_name}}", client_name) + .replace("{{issuer_name}}", issuer_name) + .replace("{{notice}}", notice), + ) + .header("Content-Type", "text/html") + .into() +} +async fn login_page_endpoint(req: Request) -> tide::Result { + let query: AuthorizationQuery = req.query()?; - let adjective = choose("family_name", names::ADJECTIVES); - let animal = choose("given_name", names::ANIMALS); + let client = req + .state() + .config + .clients + .get(&query.client_id) + .ok_or(OAuthError::new("invalid_client", "Unknown client"))?; - // Generate an identity unique to the specified client_id - // This way nobody can take data from different clients and correlate - // user information. Since we don't even keep logs here, there is no - // way to know who is who. - let mut body = json!({ - "kid": "master", - "iss": issuer_uri, - "sub": mangle("sub")[0..32], - "aud": client_id, - "exp": now+600, - "iat": now, - "given_name": animal, - "family_name": adjective, - "email": mangle("email")[0..15].to_owned()+"@email.invalid", - "email_verified": false, - "preferred_username": adjective.to_owned()+animal, - }); - - if let Some(nonce) = nonce { - body["nonce"] = nonce.into(); + // check redirect uri validity + if client + .redirect_uris + .iter() + .all(|r| r.as_str() != query.redirect_uri) + { + return Err(OAuthError::new("invalid_redirect", "").into()); } - Ok(body) + if query.response_type != "code" { + return redirect_with_query( + query.redirect_uri.as_str(), + &[ + ("state", query.state.as_deref()), + ("error", Some("unsupported_response_type")), + ], + ); + } + + Ok(render_login_page( + &client.name, + &req.state().config.issuer_name, + "", + )) +} + +async fn authorize_endpoint(mut req: Request) -> tide::Result { + req.state().total_logins.fetch_add(1, Ordering::Relaxed); + + let query: AuthorizationQuery = req.query()?; + + #[derive(Deserialize)] + struct LoginForm { + account: String, + //csrf_token: String, + } + + let body: LoginForm = req.body_form().await?; + + let client = req + .state() + .config + .clients + .get(&query.client_id) + // only devs should see this error + .ok_or(OAuthError::new("invalid_client", "Unrecognized client"))?; + + if client + .redirect_uris + .iter() + .all(|r| r.as_str() != query.redirect_uri) + { + // only devs should see this error + return Err(OAuthError::new("invalid_redirect", "").into()); + } + + if query.response_type != "code" { + return redirect_with_query( + query.redirect_uri.as_str(), + &[ + ("state", query.state.as_deref()), + ("error", Some("unsupported_response_type")), + ], + ); + } + + let account_code = accounts::normalize(&body.account); + if account_code.is_none() { + let mut login_page = render_login_page( + &client.name, + &req.state().config.issuer_name, + "Account code incorrect", + ); + login_page.set_status(400); + return Ok(login_page); + } + let account_code = account_code.unwrap(); + + let pkce = if query.code_challenge.is_some() && query.code_challenge_method.is_some() { + Some(PKCE { + challenge: query.code_challenge.unwrap(), + method: query.code_challenge_method.unwrap(), + }) + } else { + None + }; + + let mut code = [0u8; 32]; + SystemRandom::new().fill(&mut code)?; + + let code = base64_coder::URL_SAFE_NO_PAD.encode(&code); + + let redirect = redirect_with_query( + query.redirect_uri.as_str(), + &[("state", query.state.as_deref()), ("code", Some(&code))], + )?; + + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + + { + let mut auths = req.state().authorizations.lock().await; + + // clean up old authorizations + while let Some(s) = auths.expirations.front() { + if s.1 > timestamp { + break; + } + + let s = auths.expirations.pop_front().unwrap(); + + if auths.auths.remove(&s.0).is_some() { + req.state().expired_logins.fetch_add(1, Ordering::Relaxed); + } + } + + auths.auths.insert( + code.clone(), + Authorization { + client_id: query.client_id, + account: account_code, + redirect_uri: query.redirect_uri, + pkce, + nonce: query.nonce, + exp: timestamp + AUTHORIZATION_EXPIRATION, + }, + ); + auths + .expirations + .push_back((code.clone(), timestamp + AUTHORIZATION_EXPIRATION)); + } + + Ok(redirect) +} + +fn parse_basic_auth(header: &str) -> Option<(String, String)> { + let v = header.strip_prefix("Basic ")?; + + let parsed = String::from_utf8(base64_coder::STANDARD.decode(v).ok()?).ok()?; + + let parts: Vec<&str> = parsed.split(':').take(2).collect(); + + if parts.len() != 2 { + return None; + } + + Some((parts[0].to_owned(), parts[1].to_owned())) } fn create_id_token( app_state: &AppState, client_id: &str, - uid: &str, + normalized_account: &str, nonce: Option, ) -> anyhow::Result { let header = base64_coder::URL_SAFE_NO_PAD.encode( @@ -263,13 +297,13 @@ fn create_id_token( ); let claims = base64_coder::URL_SAFE_NO_PAD.encode( - create_id_token_claims( - &app_state.salt, - &app_state.issuer_uri, + accounts::create_id_token_claims( + &app_state.config.salt, + &app_state.config.issuer_uri, client_id, - uid, - nonce, - )? + normalized_account, + nonce.as_deref(), + ) .to_string(), ); @@ -287,7 +321,7 @@ fn create_id_token( Ok(format!("{}.{}", message, signature)) } -async fn authenticate(mut req: Request) -> tide::Result { +async fn authenticate_endpoint(mut req: Request) -> tide::Result { #[derive(Deserialize)] struct Body { grant_type: String, @@ -313,18 +347,20 @@ async fn authenticate(mut req: Request) -> tide::Result { return None; } Some((body.client_id.unwrap(), body.client_secret.unwrap())) - }); - let credentials = credentials.ok_or(OAuthError::new( - "invalid_client", - "Credentials not found in Basic auth or in req body", - ))?; + }) + .ok_or(OAuthError::new( + "invalid_client", + "Credentials not found in Basic auth or in req body", + ))?; // authenticate client let client_info = req .state() + .config .clients .get(&credentials.0) .ok_or(OAuthError::new("invalid_client", "Unknown client"))?; + if ring::constant_time::verify_slices_are_equal( credentials.1.as_bytes(), &client_info.client_secret.as_bytes(), @@ -335,7 +371,7 @@ async fn authenticate(mut req: Request) -> tide::Result { } // check authorization code - // this actuall consumes the code. It is not possible to retry auth with the same + // this actually consumes the code. It is not possible to retry auth with the same // authorization code. It's always the client's fault, so we don't care really. let code_info = { let mut auths = req.state().authorizations.lock().await; @@ -354,8 +390,7 @@ async fn authenticate(mut req: Request) -> tide::Result { } // verify PKCE if the client used it - if code_info.pkce.is_some() { - let pkce = code_info.pkce.unwrap(); + if let Some(pkce) = code_info.pkce { let code_verifier = body .code_verifier .ok_or(OAuthError::new("invalid_request", "PKCE not present"))?; @@ -364,12 +399,11 @@ async fn authenticate(mut req: Request) -> tide::Result { "S256" => { let sha_digest = ring::digest::digest(&ring::digest::SHA256, code_verifier.as_bytes()); - let a = base64_coder::URL_SAFE_NO_PAD.encode(sha_digest.as_ref()); - a + Ok(base64_coder::URL_SAFE_NO_PAD.encode(sha_digest.as_ref())) } - _ => pkce.challenge.clone(), // not the best, but since the only other option is - // "plain", then I guess its fine - }; + "plain" => Ok(pkce.challenge.clone()), + _ => Err(OAuthError::new("invalid_request", "invalid PKCE method")), + }?; // Needs constant time equality checking since we might be comparing plain text. // This is a non-issue when using the S265 method @@ -383,16 +417,26 @@ async fn authenticate(mut req: Request) -> tide::Result { } } + // The token is random because there are no resources protected by the token anyways. + let mut access_token = [0u8; 32]; + SystemRandom::new().fill(&mut access_token)?; + let access_token = base64_coder::URL_SAFE_NO_PAD.encode(&access_token); + + req.state() + .successful_logins + .fetch_add(1, Ordering::Relaxed); + // give access code Ok(Response::builder(200).body(json!({ - "access_token": "SOME-TOKEN", + "access_token": access_token, "token_type": "Bearer", - "expires_in": 600, - "id_token": create_id_token(req.state(), &credentials.0, &code_info.uid, code_info.nonce)?, + "expires_in": TOKEN_EXPIRATION, + "id_token": create_id_token(req.state(), &credentials.0, &code_info.account, code_info.nonce)?, + "scope": "openid profile email" })).into()) } -async fn jwks(req: Request) -> tide::Result { +async fn jwks_endpoint(req: Request) -> tide::Result { let pk: ring::rsa::PublicKeyComponents> = req.state().signing_key.public().into(); Ok(Response::builder(200) .body(json!({ @@ -408,35 +452,77 @@ async fn jwks(req: Request) -> tide::Result { } async fn configuration_endpoint(req: Request) -> tide::Result { - let uri = Url::parse(&req.state().issuer_uri)?; + let uri = Url::parse(&req.state().config.issuer_uri)?; Ok(Response::builder(200) .body(json!({ "issuer": uri, "authorization_endpoint": uri.join("/authorize")?, "token_endpoint": uri.join("/token")?, "jwks_uri": uri.join("/jwks")?, - "response_types_supported": ["code", "id_token"], + "response_types_supported": ["code"], "subject_types_supported": ["pairwise"], "id_token_signing_alg_values_supported": ["RS256"], })) .into()) } +async fn homepage_endpoint(req: Request) -> tide::Result { + Ok(Response::builder(200) + .body( + include_str!("homepage.html") + .replace("{{issuer_name}}", &req.state().config.issuer_name), + ) + .header("Content-Type", "text/html") + .into()) +} + +/// fallback endpoint for non-JS users +async fn create_account_endpoint(_req: Request) -> tide::Result { + Ok(Response::builder(200) + .body(format!( + "Your account number is: {}.\nKeep it safe and out of sight!", + accounts::generate_account()? + )) + .header("Content-Type", "text/plain") + .into()) +} + +async fn metrics_endpoint(req: Request) -> tide::Result { + Ok(Response::builder(200) + .body(format!( + r#" +# TYPE logins_total counter +logins_total {} +# TYPE logins_expired counter +logins_expired {} +# TYPE logins_success counter +logins_success {} +"#, + req.state().total_logins.load(Ordering::Relaxed), + req.state().expired_logins.load(Ordering::Relaxed), + req.state().successful_logins.load(Ordering::Relaxed), + )) + .header("Content-Type", "text/plain; version=0.0.4") + .into()) +} + pub struct AuthStore { pub auths: HashMap, pub expirations: LinkedList<(String, u64)>, } -#[derive(Clone)] -pub struct AppState { - pub clients: Arc>, - pub authorizations: Arc>, - pub issuer_uri: String, - pub issuer_name: String, - pub signing_key: Arc, - pub salt: String, +pub struct AppStateRaw { + pub config: Config, + pub authorizations: Mutex, + pub signing_key: ring::rsa::KeyPair, + pub total_logins: AtomicUsize, + pub successful_logins: AtomicUsize, + pub expired_logins: AtomicUsize, + pub server_errors: AtomicUsize, } +type AppState = Arc; + pub struct PKCE { pub challenge: String, pub method: String, @@ -444,7 +530,7 @@ pub struct PKCE { pub struct Authorization { pub client_id: String, - pub uid: String, + pub account: String, pub redirect_uri: String, pub pkce: Option, pub nonce: Option, @@ -453,43 +539,47 @@ pub struct Authorization { #[async_std::main] async fn main() -> anyhow::Result<()> { - // log::start(); + log::with_level(log::LevelFilter::Debug); let mut conf_file = - File::open(env::var("CONFIG_FILE").unwrap_or("config.toml".to_owned())).await?; + File::open(env::var("CONFIG_FILE").unwrap_or("config.yml".to_owned())).await?; let config = Config::from_file(&mut conf_file).await?; - let mut clients: HashMap = HashMap::new(); - for c in config.clients { - clients.insert(c.0, c.1.try_into()?); - } + let bind_address = format!("{}:{}", &config.host, &config.port); - let key_data = fs::read(&config.rsa_key_file).await?; + let signing_key = ring::rsa::KeyPair::from_pkcs8(&fs::read(&config.rsa_key_file).await?)?; - let mut app = tide::with_state(AppState { - clients: Arc::new(clients), - authorizations: Arc::new(Mutex::new(AuthStore { + let mut app = tide::with_state(Arc::new(AppStateRaw { + config, + authorizations: Mutex::new(AuthStore { auths: HashMap::new(), expirations: LinkedList::new(), - })), - issuer_uri: config.issuer_uri, - issuer_name: config.issuer_name, - salt: config.salt, - signing_key: Arc::new( - ring::rsa::KeyPair::from_pkcs8(&key_data)?, - ), - }); + }), + signing_key, + total_logins: AtomicUsize::new(0), + successful_logins: AtomicUsize::new(0), + expired_logins: AtomicUsize::new(0), + server_errors: AtomicUsize::new(0), + })); app.with(tide::utils::After(error_handler)); - app.at("/authorize").get(authorize); - app.at("/token").post(authenticate); - app.at("/jwks").get(jwks); + app.at("/").get(homepage_endpoint); + app.at("/authorize") + .get(login_page_endpoint) + .post(authorize_endpoint); + app.at("/token").post(authenticate_endpoint); + app.at("/jwks").get(jwks_endpoint); app.at("/.well-known/openid-configuration") .get(configuration_endpoint); + app.at("/new-account").get(create_account_endpoint); + app.at("/metrics").get(metrics_endpoint); - app.listen(format!("{}:{}", config.host, config.port)) - .await?; + auto_serve_dir!(app, "/static", "static"); + + println!("Server started at {}", &bind_address); + + app.listen(bind_address).await?; Ok(()) } diff --git a/src/names.rs b/src/names.rs deleted file mode 100644 index 66c9dc9..0000000 --- a/src/names.rs +++ /dev/null @@ -1,24 +0,0 @@ -pub const ADJECTIVES: &[&'static str] = &[ - "Affinity", "Artisan", "Benevolent", "Breezy", "Cheerful", "Compass", "Curious", - "Eager", "Ebullient", "Effervescent", "Esteem", "Evergreen", "Flourish", "Genuine", - "Grounded", "Harmony", "Inventive", "Journey", "Jubilant", "Kaleidoscope", "Kaleidoscopic", - "Kindred", "Marvel", "Melodious", "Optimistic", "Resilient", "Riverstone", "Serendipitous", - "Serendipity", "Serene", "Sincere", "Skybound", "Sparkling", "Sprightly", - "Stardust", "Stargazer", "Starry", "Steadfast", "Sunbeam", "Tapestry", - "Tenacious", "Upbeat", "Valiant", "Verdant", "Vigorous", "Wanderlust", - "Whimsical", "Witty", "Wonderstruck", "Zephyr" -]; - -pub const ANIMALS: &[&'static str] = &[ - "Axolotl", "Badger", "Baku", "Butterfly", "Capybara", "Carbuncle", - "Chameleon", "Cheetah", "Deer", "Dolphin", "Dryad", "Echidna", - "Elephant", "Fennec Fox", "Flamingo", "Fox", "Fu", "Gnome", - "Hippocampus", "Hippogriff", "Hippopotamus", "Hummingbird", "Iguana", "Kangaroo", - "Kelpie", "Kirin", "Koala", "Dragon", "Leviathan", "Lion", - "Lynx", "Manatee", "Manok", "Meerkat", "Mooncalf", "Naiad", - "Narwhal", "Orangutan", "Owl", "Panda", "Parrot", "Peacock", - "Penguin", "Phoenix", "Pixie", "Qilin", "Quokka", "Raccoon", - "Otter", "Seahorse", "Serpent", "Simurgh", "Sphynx", "Sprite", - "Squirrel", "Sylph", "Tapir", "Toucan", "Turtle", "Unicorn", - "Whale", "Ziz" -]; diff --git a/static/fonts.css b/static/fonts.css new file mode 100644 index 0000000..5385548 --- /dev/null +++ b/static/fonts.css @@ -0,0 +1,39 @@ +/* Generated using https://gwfh.mranftl.com/fonts/poppins?subsets=latin,latin-ext */ +/* Uses the Poppins font from Google Fonts */ + +/* poppins-regular - latin_latin-ext */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: url('fonts/poppins-v21-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-italic - latin_latin-ext */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: italic; + font-weight: 400; + src: url('fonts/poppins-v21-latin_latin-ext-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-700 - latin_latin-ext */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: normal; + font-weight: 700; + src: url('fonts/poppins-v21-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + +/* poppins-700italic - latin_latin-ext */ +@font-face { + font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ + font-family: 'Poppins'; + font-style: italic; + font-weight: 700; + src: url('fonts/poppins-v21-latin_latin-ext-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ +} + diff --git a/static/fonts/poppins-v21-latin_latin-ext-700.woff2 b/static/fonts/poppins-v21-latin_latin-ext-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6b4c85da89152af88c9e47d97f3d36985b4d1052 GIT binary patch literal 10736 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 C!l=>! literal 0 HcmV?d00001 diff --git a/static/fonts/poppins-v21-latin_latin-ext-italic.woff2 b/static/fonts/poppins-v21-latin_latin-ext-italic.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..50f42b9e270a0b75759f490f1d67b93f71b213ac GIT binary patch literal 11788 zcmV+nF7wfMPew8T0RR9104@vw3;+NC0BE!T04=uw0RR9100000000000000000000 z0000R9vm(PU;u)63W2Ezfrx1fg+KrSHUcCAihKkh1%p%vjVc=eRYgXIjROEA^fFP@ z%tdN;QItti6#QQ&a6?4IKd7B>2uBGSh$13N*h!t5l$=mReLD}2W8))4_qzB!QD54J ziGh73eo=biS08q_I*hXx?$QZFTEQ?kA1+1u1B^Y2dCMtGd-U;@lo>7-!$EDt)P7A70 zb{#DapbG@&o@d8-5B~qXjlW@#^MwTzXk28aQGajKI)T*R<=VfAP3BiU$TauxN(g%w zSJGu;JD%+)at{dhZc9d1chfdBeVuk-g3+yyY?U0mNwEL_ovOR>y^@_0JM#$PN>}!i ztz=G)vT5w@D4j>%*xF|iL9&8ak4x}!tL!kDI*%}nynnB*Y$X=XtW#u5$5eDFR3TQS z^rSC|{}C$$tQXi@(gj^ea0zhWOYB+b66=sd6@k4J*>j4aywXKum!3Wqo!jSy~K zo6=Ud!B{I}LS!Nkc(En};~`>G40Kx*U$`cF~-yGxM*cFuV! zBZv|PFaxITixmr<6n~Gur$XGu)WVr|EqPnUEENv)nOf%+r7XgUOGRYp+R5(7cG2>M z*GM#A2sr*ANZPTLZ)4`l7IJV)o1*|a#3I&s!g~d1K8a&~_K5TnQH!YE{ z^p?&gUPTtF|gF+hZbg3ai#ur|4PS&EL11YYyXq~OOSapZCYpB zY?)27ZYJjV9GfF^Xg1Hrq4wnXB?hAm6|$cD!&tSViz2crR4$TIgmXvPpTJ*UFS5dN zrSn^g-Qlc`LH=ykz1<*tKhix5UrVzR{u=N(JOdaHZow<;PPRawfqbNPOD~-r+E5Jy zi^5Y+DzTP}s`&a=h2@k;Y@rL!Mt3IK+ilEdpPlG7ftm3l#`2v#tIh!|N|Uc;k3|Uc z*fUu+u}`TsH3q7kWxX290$e67yk%^C#g+~tx}+2++_WyLjRekwt45BV9kfy zoY>JRBts+}j}fr?Ac2c0XjA~{80@QQcE;3}ST3x-#32Gp15vY-Oi?~_ z&`yfguQsT}$@z2$*bO;RGpo`migiJ2qOvRTyRDa>?4Ml5 z7@PZ*+Zd^VE~+jvJh@4~w^;Cp4EAvGXK$BI51`)upNV|8H+r5a{E1*Gq>#F^3sPBS zB|<56h^n}p24cGGlyX2D^MGcWX`U~#h%kR6hsYR=Iv9RM89T#HdI5Y<`S}qtNi3i1 zO~Pw|?JCf5(p0)VcJyvwPfNA6I2V({>qHFRljANmGf>np?X^5p}LqA8-%D#4cBXFl;*AAD`=-Hw-CF z7PQmnl>i@&l4=r*6q|xvOs6nVDT1V{fXFZ^l7$`z{wQ0nDJYlRu3aW+}wlViU+|$-wkNtPB!a4#uqzeE6b#0BWN`R7aN8zSNZC2Dn3N z5MaqS__hHGsLQ6yN9lKeEpe+Lr%oE12XoF$S?Ju^!1kV*?+97#yZZ>QP~6J^4|C^M;? z6iz-(Ur<;-;kclP)dra|w{03zFC^l$I(m-)-#VF4E9 zG_V91LrkTrZBqhxJJG7OfB>*$1vOYNPB0FzDMxtoD?yKnb(4f8yUbGIn~Ch(KF#-# za@HsexN#SHEYC9##<>hW;$)V3Tb&aMq_KTuje~ilt)|7;SPGA%*i^dNThPcvLoT3| z82bre5#1pfr_;Nq&+ZZpy;Nq;jJYhEQ&6velC^~ixK?i{r#R~Twi;BQ%{Ru`ZBU#~rYj-tfjn;MLz6MHy z_N#IaaCDLqvPa2F`KsjqC==M-D32k7p=8&q500zYKehyoh#OBWHDyX9rt(hck5Q-!aKD zu;NyO2X9VMv2H*Mzp4Hll^b>bbTND0gVYWY0~~v#RbvzA7v!90{C8(cbu_YHieWBAYc`?577y#htbuG8>>8+5=a*)*X@5;?Geh5qL`OG z%t+bKxgnHa#AZ4wFPYKFxyB|BmO9Sg$+;BaIZxv3AXm&b1p1kLRy;9DTp3Koml1nj z2aYEByWhiqYjcA-nbx-6EQh&{9EgS+^Gnt_8D0jt9=faI*3`?c6|?7Ww1%~#Q@xC8 z;xFe&Ths#9B;>n4Z@FTyv#k3AW6O%7HVmN`2}2}6I9K$O2=5ua?DBdis%OEFiQZh{ zB82OQobkgP;psbN&a^Vrmaxrme%+(xfb4>*ndh`ekFJ;@VB5JK83Xm4cu^F+XLtUe zh{EW#Wy9B5j-BY+B8|7z+z_!<)tOhZ>j8Q-jd63v4Ag_Y9jJV#7OGDmSj=3tNY*^n$bke8^reYZ~j_ia$Y=Zy#lw_YO1cRfoq*12N%2wmt;+er_ zkW^ZUm~cr`zA8-vj#gDtQCz9CvT-tzVkHw*iZz8g$g(#SM`fsUrlGdgdx+=+>-#>F zdaw{GPbK8{fXSjSV+(+FTyDDj-*{2l7_8tk((0$F4`%I8e;ORUZ?H(OYW%Ra zpyaz>?!4xgyY%9`-LIeM^}v14-1+4d*Y*j9M&CVM+AW8_e(I&-n;zxl+ph$#O1xn| zc>l9^m0ode-$4bl_nt1~-TRng-S_;v;;k18t`7VFBuNH=)ui~>|d^QuX)T`t7?jL_v*c%?SCfa*# zXc`H~M+e-o<6^HJfA`(hWpnq&QfJFsW8c_mFqzz^e(d$u{3bJQzw?+++VP403>{%R z${z)@W?tdTW(g5q9!H+eF$b9ZP^6ZI)M_JK20RYnq>YI2uyBswW08UuyJqz-44OcvnVmhZbBw6=K~ z7gvlq3;x@6S)OFHvn!;WDu6VkkUresrX{QA{*bP==KxL>d} zQEren8MSg~Psc|+q8kzo-p!K=t_;jpB1ay&wBWN1c-|;;29v~Zv_!8`rpe1ItvS>B1i@wbH+LH72mq=}7zWG+)d<7Oy-H2fKBETI}u6c%VoanRKiAF1E zh+BGgQP9!Vz(J~sd%xJm72Pe>R)q2oI1fzgTTvH)+lBiOmTgkOTmSS2=h zR;YXp76~R}Ol@9~PFK*eS#W{QGnJwW^tkj1WVqPo=3xT_2me4?M5&lwRj$b`78RF? zDC}W=qXaG?p=c_OSY4-y;iseM)pP;aI`=1zJ}`tIyWCV!WovL*5;WMx<-?spS922+ zWr?3E+cw4ItZ~&;BO{CtmihE>NhP9EB;-qKEeL5Qjw#>^LE+pBcks763AfV$z)BcW z8->*V{>|NVV5Pp}!fpJW8P5E4Hhttbs}jlB`W3#GIF}}I1JT!CiPDo$r5{i^2Emwl zox~n2=Y4q|SbtOZ&)8TEJTom)XI%Te{T_5_+vo&ao^T)sHS68bG)xPrByQ0@A2x;%YWKXtFkUD{okP4o@O}(X8LfgU$Iu6=v+ZgXo9)Vmw zqp-81wr*@nN`1nWM~Dz7n7T%`>mBA<68VrZ(*O=kOqqb_Y!8DC4HTxMk-bzhcs^ly zcp<>633~|IJjHKOnKe>81tOvIHWXJ}9Pci4j)GWb@02N)8+Mu;`xwFT6U zWBo@VdnhlIw-9EBK~s);)>e8e0SAgH^b|q^lMuY1wg3Wv&6LSzntB?~oowl{&XP%o zO})?cyM4L^cJ-Lu+urW68n0?i)3y!|AdfEW=$}K*Old#E^>tPSvhw7RL9{;u?VUcS zBzz8dmDzpXM;_VR(>DJH;$q~wcq4fa^5 z$n*Lrv%P1Csqpsuy{X#g9ShD*w3tO^&+tUU!U@2`PKqW1oU-ULKafs(N>wOT9bNJN z9bM3BsIDjWYHR@KSPcbw(AUrbFmH? zko3toLg|j#cEN>I9z*zQK-8Y_E~`DYZZ}8DtEEx>!c^B+IK13!6p|gK zeCPm$pfG(tt7QVm0dW6mH(TuK>Gl(~D7}4XSSpvg*`*jCG<*vp=h#*fs7)^E3Mg+RlU;VsT=I6BWh7b zX!;zHX2vZHPb=n2+U+}I1dUWca? zp)ht0@ioDz0hk%m>8k|FRwv4c>)>{AISiu=@;yr%!dChiekt+V&FON6z)qu564?T7 zmCR>DnL?;#l3Ye9icn8tfvtm=6H-qQL#mNtt;vFL9Bdzqvu#AK_ZVKmWhSANhE(O| zvJKs9An*or12eeqdExWvrc)h$_hK6O^Hzz6lLMN7s>X0V^l zXo*#Esn@=mR!gCpEC^Yh ztP>-{ZLA#(hNv(vqkA-ypYc;Ei6V97vW-m!5i-I8&%atx{wODF_4%yplf`8WsW*oU zwvCeDggaS6q)N}{l`|7441olpkS5M|U}dC`SpsUZ)a2DfH>H~c4od=$!Lw5-vUG9Q z;Yd>`wG_NI{d>hj&uZ$<@&Kwn8=mE&9=&nYrO0v?U;6fvi+tho1&|syJ+97qY4fF; z-_g@?b&fAKeNh88W;rJRnsgCo=DOEL4iZ6=0*O>~BNW<^D1^KXAqO+o)ML3RiDNZ4lfoFcS! z3Pz(L8X7W3xNE}XrgT1y4PLy*M3>buSSBG{Oe18?`1s&Bj)`#_ltF~@jF&_=jb~vn zk?jYesc8wKSofliTFCBFVY0y4Td2a=jIkv#(aCI9r9;dr5VL;C!DVh7NG7BpSgb*5 z6~XvnY|$?{MRML65~}!lZn2oR2JD(PKZd^_CJbDF3oazDhmzNyDGlKE!D9IHo0;ML z6|V#ETgi~1AA-d|!YL3qolzOZpa~1>!(!4dX_8GTMkzyJ3Fs0#zsf0N=^M&LMz2&5 zje)8XnN&ex<9K`O#b<|Tk}>%9dQIHyFLTQ(^^IjBqem`?!qojCrf@1$+50n#Bvi48 zoDG?vqj@flU?kw?VO=eL$=9j?(%E6LWSTM8iwSgYK4ACTW`b|Jk=WE?*OvXZfG2l# zwRZ&VG-kHXnbqf5r8#lU&r;j$;XaiiNLSV)#OgSsuU4xZ(54BEMS7^>Dl3tK)Ig@n z;ssJ;gv6y*0i&3yGZdGWeK8D|$f}$&xI~~RDC4L!JS4LSg)A;FC{^;;kdP(h{4$kb z9%$+1jZYi54ULP&?VpVOkqO7a`GYxM+VKnX5K^$_BgihtW$zI=eH2!;!?u`EF(z{O zDV5VEv6T`rS6&ApWSJP&E~|h2Yl0~R;-9CAUPv9Rtg{(++)!)V{;Y zmxYPHLg~IF?y_*@Oa2}Q>heafm40Y1Z04W^I_LY9UU`T5Y56JzT9W_e-- z+P1L0>tE2txR6KFiK;r+)@EeULnE+fh~)S_b_@?#=B}Q`cFV|lz*gJq(Mru~0VZoU zTL+a;q-Ko}lTnC9l?d`nWrBUB=#sL0l9Im`jOGf*ZTi}hIxg(Bfxv{uvGV7WX0Z!e zR~cL+Tl(?-^DkuK<1+L3;_+6nVJ@w`@uThjq=lcY5AMtEkZef{%>Spk+HleSVZ)XM zk4tM;7KNI#k2_!IL_vVmZ(8^|sip3uwJ{xQeG!v~5Yn!Yg5|fser2t256IB&ra$>0CTY##a4{0C=IhO-3^r_I^9 zJYfhdXdT`pVh(RSu`&7N#u@8GEPC?e>)_y=xD+q{x56+v1thLZn#h&@|YUy?y5W=v#m#}ORlXk+=x%Kr{C8F}wkP$XbNBdaPAI#%vTlt9q#N_Km};VO zJ@E$(-$eR!cq>(5AU^sgk8FL($=)MIStWf-VeIUA0zUQX26ygtoMdEcZIU@9(fk1j z9(o_5lA$?w59@+A0=EsHvS{CioRWJ5c@)I~{M?#5Z!a&UFHG=~*+uUQ8L%Q^aNX(b zlFthY=we$)Un#5NYp=w7I8Cgf6194b>&VD z*%%p1RJZLQ?ZTAGhZ-Tos~dT^jzE9eT=__a(FKKOrL~azuJCdcCfk8+4@@cdODfPt z=&lZ|pcdN}Xe8`W#A1}m0Y)dFmX5d5{PMXtf{%dfBUE_(kQ8*kvbV>Gd~0y{u6n~_ zgt(eoYED5J={~7sM;WFw7iFqTiUFasmC4)&h$(@6qKwKUrVJuyo9? zdXbXk<>XXPS4XXUhR`}-hs1kUfaIV7{YZ(ILUFPa)R|k4#Yc-YN`94vHnrCNwTxF$ zFX3LBBTAM@Iy2>kq*m=knJcEJF2hu!6c3<-~Tus;6HD+J)@risOay6(XFiJorl07u-dWyVR1 zVc9sSp~m_w}(ltI5|AsPNUk6!%{K%s(uXf_(U8C4IxtTA;$1AWz z#EMa@6epKmCD?j&fjH&vV1R5{nFhj1rEkt1`+kD@B7#dkPVYKKSG)i@>eRQRhx3!7 zqcg(!z~$oIuUQoDl7->-Q<0vDO-ri-eji_BgiFWtQ}hTaXxg_%KTyyHjLjzrI`>*+ z!pa9_k(hKEF`TI;MfhkLprv^(4)4d;mCwBIPcfr7|G(Ubv?U@&NggsclDH*(SA0@8 zF@8;4@*EhEugzWx7wAN`0%{Zuf^%65+Fvd7qsV;a!15}`sP zRe=2asAPXBUGa5b?jL+`c;e(up2GYtUOf-z4}Y85@IP9{(ZBoKKPw48o{8+g2Q9ky zQ6yY?HZuLWz2f<~NU-eYyrSHEfiN$Jw5GUlK2dIN5n5jCWB`9M_Fu$2@JQLqAN*h7 z9o}9&z*09wNj(RRQ3`sWalDq&a%px?`>N(RUyxYCR~Z(X@|BM^{?|uWMEzHMh>|z! zb>RNy+#kRE=CslgU+`>4ZVZ2CF2=hjcYIHN-XE^4OA|$K{zPG{_gF#B(E&5gzAYKq zbdb0o#7t0e9c4!7)5p4hK*)ZG?}xBrF3i&$>$V`I7MA+=_7f!i8J(*%i1q`++0`a; zo{W-bT78P+78bu>`<~2(mB0jSx04gIF$O$aiEJfMy{H>4322;u1>kr%z%IAxd?ruM zzaa;Nz#P^Ag|);&Lv-v!?t8JjmZ_n>Xqc`Ia-4@AbakHX1q*9eSJF_Q1*gJP!yrli zYAg-)8H~^0iZnidksJ`;fbw3PG^;7p7Y#F!8pr?Zf{7q267C7hVt+n$RwCp$aKe}L z7VcjK)KFi7>Tzfs2P1+$To2p#RMf5>!BC$C*TGc7nE&KZiU0c$=vxl+&vSx*q9GVI z(wNKaNzr1oszr4>G>-M_g0|5PoD#c*1$V&ofbDcSI7(Sq|4}=SgPr!~{=Cz2mPjgH zUQeZ?%%6{(2+G=2_T!XP#b#luK6h`->5UQQ)a6w2MRqu+A0N9#2SJ)p@q_XA4-9i*T}*}-Cu!{hL&CszXWd#4k;zzGXMMQR zzIxbDXU({8_v#+!eGg)9Y_aKVFLbu^I@f)2b7$2@fkW{}xwxvMiRc__M>vmsb^@G}?VpaVw6^47>aI zjD7dH_P=|_?G5K{w}0xwIr4XH=+}1CFz0r~;zyQWGAd;LF};~{X+C$qS;zk?Jc)+0 zVx9uMY;BU5bN9w&gVfJ{EUCMv{aYk+TJ=RvqcWGl-EEu59sjJ~E*r}H-PdaeWRbXM zTky`cWo?oC`K8D}PoIg8zp)lDua)I9h<&@bwG}$-#x3RlNaYk}-_d;ZF=i#HC z-3zjO-?6Sln(TYIaX!Cc%wMftsL8(h@LMT6-iH{-Al9?o@7}k}@%h;pIG4YEt8D)1 zv;IASh4M`Hrp(S?$L@WMlMz6_>m(0o&0Z<^+5FtOeqEg#qi^=?g;KNQ%@Q4X^D4_?`Ck^~ z|M}eobtm(dx?nUv`J`U9t(%C;`5s2c?A|T(Tt4qCJBPH5kr#9^%V#oI-g3J?=1GJ) z%eMSi-1GmI&=D+PUE4SFtYty_1@~ee{+p|$`dS(~Do)cIvt{vl> z@0PnzK$CBR?_^u|_UJ;<0qs&-VtKxGZ`x>~R9j4Z;p655mbZE&0si0H(=uw~);Jho zOT@=NBWL$MRsV?!4ZRQWoA=hc0KY$Y{D1f2-KQqr3WSIh0wBP*kI{B}VDkP=;@!1} z?1ty|yjB7yl?B_ik~%=St&2&?7FaUaw#e@`xqT|$CUrTlC7(}2?yBiNE}zem=Ha_3 zBtK0{yKur|C>N}1neB#Y7kUTCo2k0J^4ml0YaxmH|5A&1`XyVFiHZQq^@_M#=V|?t zRv0MjCz1*SN%>3CiUDOkD#=?(#fqy%QVvO4PDwp0x5c7V$i^`>&srr&C31BeUbl0t z>)dUge<}l&IR9psioA0~HYT=PY=4#!7=S$f%rc%-H}PPC>BtpnJ>i`ZV={kDZYV-q z1jvk1$+GJ6#2ZRCPZ|Rf6I6*u#%MX#qG09MsVrGa&d;e_)s-x%vSW@n62{TW6l}I+JZ-=Q&dV z!00kv0hymAdpEhj4ndg5=$$5O9Ol%$j1l_08VN-J7#&FH8QPkXkiksc5N1?)388;w z8-#pjGYFVxm?={->&q6cMP?~czkUly=n8<2q;H;_(bEndY~jg25b0m=o}E)uz<78Z zU{=w_Q?W5*t`f|%NE^-*alK&1&1v^Ut^_3{T2+o2vYrtHYYM9Z07&Z!D`PHmA1I7! zrp#z)Q*pI2idNoo-`Reo37!0*S;FJCgxM?h*eGHpYfDQy27tt? zR3uLGrXD`t8fHCP#X^Edq9w=W>Eu=s^x{CaP2{*j3u%q#F??LU31C2Ow;jTe2rwa1 zMF8#rEFpkh1%!fa(ExbIWo<_olruYqKsd5vD3-n*!=!NR7%qn1A93uV6{64p59lg( z-KG;uX<;X?4h`C{StnAgK~Vk@wdzo(l!k+gPvkFA0lKwtOU58`RRVXWC>5K<6^EZuYbs*Nr54J1D*DXnX z=U{Wr&ni);VT*P1Jmh<|YF2vW4`_wmTKIJ6he=ELERDfe7k6U5Pyy^#klTS2779WW z1`GdYl>hb-C0YzP1SAx+SQuD1apEONgh!AhSqh?5Y0_oLlqFk^TzT>pC{(0a36fG} z%2gnvprWB;U}C9Mg^h!YhfhFAL`*_TMovLVMXj2KmX4l*k%^gwm5m+5!O6v~Mr|U% zHnYrj+A81bpeb8xE@T+%n(z%Gc^W^2B_N8Z>DZ)MB$%?b>wc)U8X8UJvz|qThh2 z22I#vx@l$@^4L=+5)F%i!C?p(5{3frybl@{>;Hi57i@5W!Qv91gd_$ZLXwi4lo0*p z@1!Oz>B&fDvXY&gSgA}^*x`ih zki+4HA3=l>MI1?_kwxydJ5fX#Rn)1DCfewtk0HjGVvZ%&*kTV9N1RT^6?bY<>#lq8 z*coqp@h6bF)TbehzVo{u{p4rg`!!7ora3KXOzJ+#rTK|8=Gz9VpXwyW+O&sLFe(ts5QU1{_0je4a`;-JfW2+=jhlQ;69 zef>3{+kb9-)Bb3|d*D~Vqc8j$)?-G__Mh-Aw-~+1kh)_&RqlG u^Q(E)KK^kr*UZnkfcrD@AE#BHc4zzY%HaGI@AS#1J9d;}l`gIouVDjS$j72}vyz{UYE^vJcN zs9B5Dm!c?>oGSbOV}guv+v9!}K~&vcqKw!a)>>N8x~I}u`%2>Jr0OH1;%VO=$0@fm z&q@3RadZd`Z0S=AEZx|&h@;fOe^GAAm$M$H+*JM1MwAjR_||s`T;vFieUi)5!~S?W zGxz<4)fJ^`M>i;Km6SGgIHYax8=jwA4^UbZP@yHph*hwRzwNK`FW5#61_MT^q@0*_ zSQv$RaN*rC`zp=4&^s@Ty!J5iysi&1Sk!4jOYBH)6k^pS9K*NcqT}0fy|a2AhurJbjr#@C4YpVN|B_x0z-v+FDAl?7ny01`C8hg`H(dfUBetstBE*iKLa z6Tre64-3FSJOt2~*)&!*t?EEP;?gFM*ZI1~G|$f3H|RnqL>=NH7g@-n^MCAPY_`m= z)W!iO^c`+@Q2<-Wl`HDRlVKBxHw9o#RCdI|ipk8p@~TcU&72)xxYG--d>La_n+o*a z+5l&ytXzy72+&Snt7>htw3MmqHW`}b+sum^XrJc)S&%Nd z)JK)Kdyo7lf%V1uqkBH~q0^2x+4~*o?cQuh+oQ?(?N>d*{CuDO!L-pjl`fn+b84>^ z_4~v8vf>x$tUgmL?hm$&s*QY^nM>Tjeo2mJ#~*tdJ)1tx@;)jzU?#W^=y7fs0vI?Z zhe|i`(I9hXY+GgYEYx&7c%9stA{}0%lT~a0gYn9RItgZCgcicA@pNQ1};9{jYTL~m9oOle43^odgnylP;X z&3>e}Qy_pEX09tDL9^1p96o;aw9hMciY*Sug>h@5iW~qZr`wSlhFZ6%$+HBEj`5gL z#q8^yid{q6Q=nXch$F5eE>tL-RO>>R#t_v=+c+t@bmbl4m=nmNrUKxKHaF%fB1gkC z(qO*biZ!Hu1pniJL-s6>A`U6%;n6o#YHjqzM)AI30?M_HxENZF3|CyQltmPdDF#GD z2Lam|Acr&~@-W8q?VGXkH4;8OR0|W^d8x}CZc$-Oh2j8h2P+{!G{12UZKjoh69e?O z6^-yKMo-wPs@3mTm6}-MAonngJYpJ1e$1RZJmobc&QTU5R(O_wVVbQRoel=m7+LOx ztTdF>AR>^oog+$Ej>vuAAm`EcyX-N$TqX%BQoEYvG{^D`NYiIZG@hKpA)HAi%vuMi|0*nTi6(Tz08 zu)GvmK1&xD-fmo3E?L&!{AUP9cJ6Ijr|*ze+2!?03Y$L0B&p3W`ai^NeGwARn0G7w zF#T8l_El$hzk)w3)B0V4WJ6V1k4B>|eEYnA)DvZ(9SQXTDL@71>y;j)65_V!_9 z9vz|d_(_R_+}EpwfIvbXxsFE>xdhTl^Q^*o%4Lt*1b7|nj7ws>+S_h@K>Fe+?+w`9tP1s~doq$G~4UQ1%? zY*<2dS#~@rGtmq_Ye=i1T92o{{IDvuiqvbU))7}Mv8S?85)jbqK%bFJ0m<}{Pw~l! zFs3p#1B+{NRZE(OPM*{_g~gJpj;6~h6P%(Jw!WFnHl|`NL@52k&sSb%?}yzrzh}~? z(w$#uW!%YBf_tG@Vhh9Dk5N_wLXTgH8W-{Ql2!$IkiuF^Y`tNGWc^){T|Z0yp!=O{ zCT&xao4=w1PdB*9V0=RAO#fG-C+MWJko(zvUDorlb5qPItvp*xrh=fajv%IpEz-i? z5Co6+X#P*B_aT7X?XY;N(7oApE6Fg>QLzno5ycV_wIPp%V|6s7h0hvtC$+SvPkLI3 z;Kld`m1;as>aAs2*jevlD(Ny+cc+1GwBOECZG3(ks0LG&?E-w6ip!9Q0(yXPUBS>) zIC;#cGf0bSoQ;LKN5=(SR*i|#4I9d?s2y#}l4Bb;QkHI!>jR0iLG4he%I$w8?`m}( zmY3R&56FG)$fW5K#>yL^NcGcq-3gKvfZv}O;0#Vmw${%Z8s~I+kUww?`RWk2{{(#j zQ$yJ|B$|aF^(ukfVfu9)1y!B!8N_B=jN8zoT-SbtGqx1$`!C$rB+VrUF3(C^X{cz==8kg|ep>Syd*L zzfh&S(*v|211B2#up}|2QzpcS|Lj z!`0+%nxtRM!8Y}?W!(6C0OnOCk1L0z0Q7P2hm^w}WxJ>(Hxwf>f!Q3VE?cWZbS*=!y z_bF#70DxfM*$h@4mIrg+g}(#e#Q7_#r_}|@kytVj)$&?w^z{W~-M6ADrSf|pFIX0wt?C8A zh8_SJDp8yK*|`Iqo_mST=is(Mm}`ly#_&o3$Va}FO6lgB1YfEEvZFOo4;1UDyL<{kxOkOA%C%wHwVo%Sq~QWG4D#3R;F6wB z&N~(A7z(i(F_cOc;e#boDF(%`;l>IbfrY+qp>}Uk)}dTn07xkGDN>q!)5}wfjK?Qt z;H>uR-;|l&VUrrpJ7lKQ6R&vPX}lj?u;G(v$Lfsq!_dNw{u(++@)-hq=Vwy=69}++ z6wFlrH#tj&x(Qq0kxG&2J~HqU{$e2q+>ua!9&sN_3S%jz?U$+Rxj|ggi$SZcOR`ys zmr1i?^SmR-kir{~41rc*rr7iZEyF>;etC8Iz!I&bW{Hhz9iRCR`ZGM=wY!&?#$J7f zVT8b{|9ZUaYqJ9Kci5Vcl#|7w_vbhYkjVF)^V%GpL&GXa#^uYU@bJw4DVum)nZu>w zC~`+B%N<5y4Q{TWl1|%RnlEWfJR#4jAmfOTvw~-Da!%2?xclgo?Pzc=Cv{LX953Oh z+X_9}MV;*nOu%1PQ{}I1@~>zMrp&08iWF%Mg}6XG2tJ%ldgeT(jJ!!q5o^YyduJV;qdusE55 z7(HF@h-<9|YA>@X<8RRneKAud(6XDwVaLpMEk9GQ zQf?jP2=SyNq2EDySg#WZI4qIAYozie>m!cg@_Mn&czmd+s0D}bEBgIJf!)OEL38+}`QJG~kk{Jju}y8G*DB|akANaM-u%clyat6VUX;;5r2 z8WuIj)z})1Dwzy|SSr7;CIikyfQ!YWN%#VuieF;~r8}hVdrdd1wbeTvJ7c~|?wJM> zhdg&f_A;umPJ#_`AzEv*&=44GoMyLwZsZHJTDL}{HEC!Xa*pNocfipNVY_uP*y?X! ziR$L{w@Rpu4ScvRjNo>5g@7*QqefsanIo>JvjeKe`3u^Xx4e6#54*d;?5b^T ziZhwctT7wpDkB2R3|2^?w`zc7(c^oM?}ne;eR2;3|5r~PcUc{;1}?*2Dct`dydTUB zxdVgMp=h@@W?P;=#5Qu|QZ7#>Vp%vehmRz*eb)E?rcDg&Hd;6OwUpI9yNIl3{pu#s zV2e|$;ths0a!-N$4K1mlXaYpQlzmHVu@Ai6nJ>X2$_b5dL zjb^GP%BDpAmizG}Etx0Md81Gx0x6qDld?sd?iQ)|OqNz8-u`kYsQ3`swH@)cjWwaG zo4yT-VMSOh>@gXIJz=o|4vM~QLi@*pZQkt;WM?hdG`Y`Y+t%1<+q%!tb}e#kxM{`8 z!38VV4ELe>!2af6Ua4L^u2o$}yHw!vq>}L4OXW)z5&_PKUpI*qiguBx$6yf6Zx<^g(4F;!o)ZyG&TkG64>hOjRHlh8XW-@|s{2U~Lko{z^e=>qZz@f<) zOQ6G=W0NCB%Tw^0_=Df{5pc!fYoG0s9rtmD&gbNYO`*AXDze2!5I}wSL6+ zKRdQoq^Saf97USSD-ci`JL+)mq9^*;h3u@WLvm?8NYgGB^%_m0p0HS{1cKjWG?Ck1 z0rEF=w#^$Zw)}Rjjy-*HA$M^7FxXH0{Gp{waWpTn{?*n|7&kE*+@FAlVJH|>|{X4z1MB}(FE+ZkuN4|cM|s#tHFm6?By!UG?v0>{SE*V`P_0SY5WV9%qjIsz6PaQ~WQ{w>M#?iSEfVw!0J_f|_TwrW8| zow%=4s(=FA?bS|{vv#6^Cs%aH#e>E(91>z8QYc1(bny5jr}2YKM7NO#K6=>5l`1== z;y#m6JlH8$$Q!s5HOO5A9d0a-6DZ`82C|dOV~=9ym`~$YcS|)j3%oNezH>SzN8@Sm z`8|yqj<)hg-z?)$wO^A;iTf?wlvE0uSZ9iycb#=R0@eQLd_D=1vF}aMRXZTIhW?*# zmS)K3)6A1f2h3)1|2&CK(PQ`N2J17wj{ILM*4L(dY-L5d`#Ke)9A;yLZk% z?T?xfod)**NCWNyax~sgWmbN=I*U|pbXf3*?-)TL+Jg>Ur8L0Z5(w~x;23UxcN;5@ z0t(TLIdKf|Fuq4B8!(%ofnJG5(<_1c&1U65zf?oinsHP_B%&ZjEQxHul0Ggm5z#k+ z4<9rP2GRGT%sg9929PqOCLEkXkBai5kHm^3f z?BGnizk&%Dx)~A*gLPP(kQgme@gt2iQj}99`F%o>ua3fz zc$_l_j2TiZH$Q4Gr1qU*X zV`sdViyICbA0W4(4i>wI-q!ycbRQ~q^?tSJE7xmZ&;J@U4t~}1mFLarZ>HA`emVb3 z&qT_bGXbi2=5DmRG45tw&7oss$G~dkG*yQxVP(9sRSD`}uSv6@6OaTi7SgFCENywI zp)4=Xl#&o&X;fKbbR$(x!zOrA;#6Llp|o7flITKpe;3N;A8+-~69vKWyaQ-{E^} z`Y@Y){A+z1llY4}PF7Ygw3F%~4Y;S>d!^dabi`iAJ%)?NY2yP08eo2}Hh#$%GA&z!x&X@-EMKGsEs1pMki?Wyj~W0j`OQ$nxtDV0?g3 zDNYuP;*tQ1Jv&_vr=puu)iv9Tp`F@ z#OSMn%0PjXpf_QuG$WQYbQ;lM!eOETcSB2=0wJEj6S7%c0Rb-*GQoPZZ`|J6soCS+ zLl~4)(f$UhBVswO=l*u06q3GQvF(ZX0kLDec{~K{i|DCm|1NtlVr`0i)!!ZmAm$Q; zme(TYp~%K|0>uhKu^;1kmT2c^Q2TZPOzWe6W&ge?p^hy{md*a9kqM}M)w&VP6+KzTSdz&0n*E)?`eYJ?U7>ps@+thlEWHni-*A}*|Si4}siZ#Q1zn(Uo z{;dyuw_*Dx9y=PhY}vdO;xP=|wjHd{jpKSC40+-F7W79vv9c;Kik~k>gxueOwV%`F z&8x%=F9Ph0YM?=(pDZ{`g8V5EGixn!6j?L8e#`A0gd|)ZBvlQ^4g>az&h~bx4@XvE zi3HKweLXW~-x17$GK4T2sZoQFy!wxpDQjT5Nj|F=#3O8pb_Up`2^@Vv;&f)p3#RnWHy<(d~oJ~rOC&>96}LU`VV zN+|wQ1~K9xxZtbio`{4!P5^xHbAtpz8XqPoARFsJ?jzK&YjRCBt2rDOnj$AsibnpT ztPI`yJEp%iGvm*kxg7!Ar(msyIlUzm8IdCQQp}*&WJ1Gkk#FeyKiv zy@Z9PV6yEw86=DIx3v&wgLO`>tBlV?>_fDrh2Elf>bY}NlzsczKMqHNdv&x+l^Yha zEYmP?PO{A=N<9&)=9W48NPEQbbr7>#gOW#lMEP5Ong$iFsUR}ahz1A97{a#JR1oMH zL=*aZLW&sxsodc46Brk$+G8Wkj&0y zdm;hbT4Qe)$jKmE8sSMKG>2Y@umhfrBl-dON8~PYh&h*M!HjD$3mlNjG*o@ARG-I3 z@`GsEAx@t)t7JLth<|J69~sZ{96Hc{Yr6Y5x*pda++FaW{J-ZF z)VT^RU#zydw>pqrwROl&xeAwUi3%-S)+x_%){5o_*fZuE3~IYvTr3crOJ*F2(R-W< zP6+mH{K|IOqdD_rD$V>5Ls{nWwk@vf8M(C9Cs`6wA6%Ah#EeoUjR z3UoBntJolTHgbVDN<~&xdK$@s?D=j<3xnPVV4a){)(=|J8qC7GU<)v=l-8lvN#sGs z@?iEFZJGmN;M9zuqChov;J4G8?8cq-0mBY?Hu$U7!TNP|c?I}r88=wR7HZm5vqgb| zy0Pv~UPm33R~;icFJe~LS4u$i+l^vPI9AbKnHH;13FVow%JjVB6hoBN=ViGj!CtSg ziB&zWiB%eeVtzS-%jJj`6ba!TP<>kZENPq*$M`efn{c3lT}n0?D+m49I~EJg!Q9md zLHA8Erkdvm7i<|@YJ!#;mKY#VvTuogsUBKtT4DmYT}ntSRza*(I(j1kzY$GO1EdiP z1S)8`H^JRapW|`Y2!v}m{O3f)!;FlFSy_)VG9G1tj5X4-^uw5Ij%leKD5~E4<@!4J zukw4Xri2F6Hm+zy&l%y!M|8lxUY()_C6`etx-kujxHaI<$odF! zys^OyRZ#eKu*5$WZ0&q!urTNq7|XM>N{#%gSx{B;Fh|=u)>Kj*{H9R|n|jRIs^9tl zmS*MA{}KF_tLQQ4!MPnb-?)5ISvBkNC|Wd%uHJ>_?<)Jz{q>s|(VG?3hs#BW%TK?= ztsIzm16(Ysx~{lB51D6CSgI_LMe~sIrEKkNo3~Ittb!z5zFYzigUnM;8Rjwwlv<8e zR*CDw;MJitHb!)*E0`GC0$jgey=nkxT)zTvfONp7(t*6LRJzQ(AzJG0Jy2;Pl@l?^ zE1FH{yHej+lPTTNFuga+^9J;wch_@WMTK5*9x2@k;D=ElpT$F$t}m7DNSN!L?SkuV zP)yNrN2O}wNa>D->9NLhf4iVZWtJx@OJ$W=E`TSZ9YWR_oc$KaljZy-(F|>e$&0hc#bru&g5q9iJlnPl z+5-pZI6K}&rqDvxA^_YXY@p2G(OW?TXTw%|eKl;YawKeTZx7o$D2v2$JlF(C8;2>S z^_)l@lwEay{fA~)p0p%n*8}C;`51^kM1?it3Z+-vT+FV?!^+AgHwQW1w=B)GWx4_51n5Mc;$(}qhh-nAj?$OH-Mr9j2_Z6Y2cKuje~~N)z<9 zuzH?0^;+)L&G-ut`|goifvgxy<^A3dg$9wlXW$!r>LS z7vH^RM`foQo$IR3^_kB1E%;s&XD>4fBXBMv{qJ}*w1f<`Oa0qUw+mVf{`ht&JCB2N zmx3P$`V5}TZVO(S?}qcZJ9nB~TFW7Eurtd3RxPLvA3++rjWJbyUq0NOF(Z0+T!({! zY+4`mAMqgl_|5L(1{ZXm!C&xh0S=VznB=F+Yj#R{G4qc7GT{FHW49}x`Qzs^eC!T? zqW@c;pQikKhW!7TSJmmIVFk(;*#Nu#ZC{IItgU_A;?ln&B*R$NDFk}wj3yprJJ+x7 zLuw#j%_tAvTQ3gq1DY7FkI~*p`RfZ2PyL-d}vD(N}Pc=10ski-P{aXI66?wEkJj zqy&9@?-j}a|6fVSAT}Fb!(thq5chW;V*lU$a`vWLhGc!v^E_-HS-t-73`kI?Uu|So zMq3C?&i(w5eSDq=>?3sVNb~=nT1i>@V)EDfr}i0p+Sjln21 z=|}bOKz^N3<#PJTNrChQJTt`vBiOKXwuTzTbuZ|KEfE zK2D6A@^#OJhz1bgTNwB^e~_5)SM!P>%6&^N$@7=qEjNEdm$RrlA0-uRK`98F#HiwYDwt0d<|o`9V)g0zs-;-Y!{#Ph+wmfRE+3_8q;@E4J4NSHv`AsQaN0@X z5^Ul@Mlnpipr$$8O$H_N0@Pg!TSGll0Hz;NHwvipC-o!(Opj1|N!>K%wNmFz>Tyx` z6!oORR7^cHVOm996=w4kbvIS_m_27@v-%74K&6^BG@~Ny4T3qq zydc4t(2GjU*w!Afv6P+yMpkxvGnYXMPvS^aw0PP5P34!BzgPSkB8&ZwR_ewu{&PG- zm6PGLmI5h7_q!7OGY{~DO5sWKg^Iw!V=rTUm@Qg8;78TRnk2(J$BP!<9i8-)>}aP zUhs5ql^Zg5U*PEaDm^mMXF}X1S_GS|jFnO%6OLYr@1>x=1@mN%5&Ox^i;o7#ux2Pm#~P^HhmU zQf#P_E~QKzz_r@<-L?p0Asjo*uT4M;%?!gQ0)irira!+r1X?mMosbqS0-A;47}TiK zA;_x@6&?{0RkBY79|Z*y87;PgGpLe^EAe4m(;Bfj!*fT#b;ETPAR}%P1x${0O*hD; zHj)~`*e^~9bqKB>8fw;MKGoC-+$1{qrd+>}CWJ$&_S4a!S@mzyT%z6hkS2 zR;o<73K&>8cmzZwWE501bd?yGSlBqYc=!Z_M8qVdWaJ{&S?Gj~?i;q!GTV&VlQ^){bwk#9>X{XZ2aA2=wkNhb=!NHA+T{&j z_{=yTztyUJrN-yJbjH`d@vUFfy5OvHCe(Rsjms{&q~32HSfW8dqb5Pkb_i+JqD@$b zcAdKXs@pts&DX2Pq@Bj?*Jq%?e(zyNB^b+g-2Uqo-|K%82u1q7L@JXjlq$8RKl@9o zBgU+WshPQjrB#1-MTL#6oxOvjQ)kZmp&wCQx^nHttvmM~JbLono4rLqNJLCRN=6Q% z=x+BYsle1Uv~=_gj7-cdtZeKY5KgX+ar3C+?Wca`Yo8?jsZLF5QcTkf z3A69rU4WVAJk9$1#EA7}rxx>PWt4NmiZ*|idCbN9T}hn4oCqu0QXUG@_u@1$r(X!H zYl?jYI?lz~fTV^1#i#qOyGb&oT#SD4!OroxMu?9a1thgAVXRT?cXq>0{5OF^LpaS} zO>IjUqBIvrQ^?o|FgUl*@zu72qHk~#J6xMtHi1iza0#{Tsk?y630!4L#p5;w#-{{s zPjL06lYUBO37)Ey#4mJZNommK-C9%HR(L9;+a|5m?O#c^`KNzZ_0Q|@BlMfVa}@el zv}Y_<`qzE)EvDYcj0RRByC+@%i literal 0 HcmV?d00001