Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for CBC, CTR Cipher modes with AES 128 & 256 bit keys. #150

Merged
merged 55 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5f89d70
API changes POC
justsmth May 16, 2023
1ba1b6f
CBC & CTR implementations
skmcgrail May 17, 2023
8fe49ec
Minor cleanup
justsmth May 17, 2023
0912be8
Add decrypt AES_KEY; add tests
justsmth May 17, 2023
2394ed4
Add some constants
skmcgrail May 17, 2023
d11bfe7
Fix clippy for FIPS
justsmth May 17, 2023
ad64588
Make certian items public, padding strategy
skmcgrail May 17, 2023
6d7b128
Fix format
skmcgrail May 17, 2023
5e6ee99
Fix clippy
justsmth May 17, 2023
b80b8f0
Change decrypt return value
justsmth May 17, 2023
db0d53e
More unit tests
justsmth May 17, 2023
726a118
Add OpenSSL test vectors
skmcgrail May 17, 2023
6bd64bc
Test case names
skmcgrail May 17, 2023
5cd2811
Add documentation
skmcgrail May 18, 2023
fd4f1c0
Minor doc/test adjustments
justsmth May 18, 2023
261db7a
Renaming UnboundCipherKey and NonceIV
justsmth May 18, 2023
b1758a3
LessSafeCipherKey implementation & refactor
skmcgrail May 16, 2023
692e051
Non-generic public interface
skmcgrail May 19, 2023
6758847
cargo fmt
skmcgrail May 19, 2023
a36cee9
Implementation cleanup
justsmth May 22, 2023
4150749
API Cleanup
skmcgrail May 22, 2023
c8009bd
Start of benchmarks
skmcgrail May 23, 2023
1f253ad
More benchmarks
skmcgrail May 23, 2023
f1c8096
Rename Unpadded to NoPadding
skmcgrail May 24, 2023
8f95f12
Redesign for CBC/CTR
skmcgrail May 25, 2023
40c8028
Clippy Cleanup
skmcgrail May 26, 2023
5a17f38
Minor cipher refactoring; Add integ tests (#141)
justsmth Jun 6, 2023
c72d041
Zeroize IVs (#143)
justsmth Jun 6, 2023
89ff782
Add Cipher CLI example for CBC/CTR API (#142)
skmcgrail Jun 6, 2023
af189c5
Improve test coverage (#144)
justsmth Jun 6, 2023
1e02c29
Remove TryFrom conversions for padding to unpadded (#145)
skmcgrail Jun 7, 2023
a5653dc
Examples use `?`, not `try!`, not `unwrap` (#146)
skmcgrail Jun 7, 2023
e8f11bc
Support KKDF; Minor doc improvements (#147)
justsmth Jun 7, 2023
7d59447
Implement Debug trait for Cipher types (#148)
skmcgrail Jun 7, 2023
561f28b
Provide better documentation for CipherContext (#151)
skmcgrail Jun 9, 2023
586018a
Update aws-lc-rs/src/cipher/key.rs
justsmth Jun 12, 2023
01e49e3
Update aws-lc-rs/tests/cipher_test.rs
justsmth Jun 12, 2023
c134c7b
Update aws-lc-rs/tests/hkdf_test.rs
justsmth Jun 12, 2023
3599aa3
Use MAX_CIPHER_KEY_LEN
justsmth Jun 12, 2023
fbb03fc
Use 16-byte padding-buffer to avoid extra heap allocation
justsmth Jun 12, 2023
2c2fb71
Satisfy clippy
justsmth Jun 12, 2023
c565627
No branch needed
justsmth Jun 12, 2023
fd70cec
Spelling: Initialize 🤦
justsmth Jun 14, 2023
7a05c7c
Fix typos; Rename redundant tests
justsmth Jun 14, 2023
66fecc7
Use constants for key/iv lengths
justsmth Jun 14, 2023
2aa6844
Separate constants for AES CTR/CBC IV lengths
justsmth Jun 14, 2023
7438df0
Remove unnecessary from FixedLength
justsmth Jun 14, 2023
255c1a1
Run openssl benchmarks with openssl-benchmarks feature (#154)
justsmth Jun 14, 2023
3681904
Update aws-lc-rs/benches/cipher_benchmark.rs
justsmth Jun 15, 2023
4eba1b9
CBC/CTR API Improvements (#155)
skmcgrail Jun 15, 2023
b3aab2b
Avoid using transmute
justsmth Jun 15, 2023
c766fbc
Merge from main
justsmth Jun 15, 2023
a1061cc
Copyright for cipher_benchmark.rs
justsmth Jun 19, 2023
ca34735
Use constants in example
skmcgrail Jun 19, 2023
2987a36
udeps: we use openssl for benchmarks
justsmth Jun 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ jobs:
args: cargo-udeps

- name: Run cargo udeps
run: cargo udeps --workspace --all-targets
# we only use openssl when the openssl-benchmarks feature is enabled.
# openssl is a dev-dependency so it can't be optional.
run: cargo udeps --workspace --all-targets --features openssl-benchmarks
env:
RUSTC_WRAPPER: ""

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"aws-lc-sys",
"aws-lc-fips-sys"
]
resolver = "2"

[profile.bench]
lto = true
11 changes: 9 additions & 2 deletions aws-lc-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ default = ["aws-lc-sys", "alloc", "ring-io", "ring-sig-verify"]
ring-io = ["dep:untrusted"]
ring-sig-verify = ["dep:untrusted"]
ring-benchmarks = []
openssl-benchmarks = []
bindgen = ["aws-lc-sys?/bindgen", "aws-lc-fips-sys?/bindgen"]
asan = ["aws-lc-sys?/asan", "aws-lc-fips-sys?/asan"]

Expand All @@ -46,11 +47,13 @@ mirai-annotations = "1.12.0"

[dev-dependencies]
paste = "1.0"
criterion = { version = "0.5.0", features = ["csv_output"]}
criterion = { version = "0.5.0", features = ["csv_output"] }
ring = "0.16"
regex = "1.6.0"
lazy_static = "1.4.0"
clap = {version = "4.1.8", features = ["derive"]}
clap = { version = "4.1.8", features = ["derive"] }
openssl = { version = "0.10.52", features = ["vendored"] }
hex = "0.4.3"

[[bench]]
name = "aead_benchmark"
Expand Down Expand Up @@ -91,3 +94,7 @@ harness = false
[[bench]]
name = "agreement_benchmark"
harness = false

[[bench]]
name = "cipher_benchmark"
harness = false
158 changes: 158 additions & 0 deletions aws-lc-rs/benches/cipher_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

use aws_lc_rs::cipher::{
justsmth marked this conversation as resolved.
Show resolved Hide resolved
CipherContext, DecryptingKey, EncryptingKey, OperatingMode, PaddedBlockDecryptingKey,
PaddedBlockEncryptingKey, UnboundCipherKey, AES_128, AES_256,
};
use aws_lc_rs::{test, test_file};
use criterion::{criterion_group, criterion_main, Criterion};

macro_rules! openssl_bench {
($group:ident, $openssl: expr, $key:ident, $iv:ident, $data:ident) => {{
#[cfg(feature = "openssl-benchmarks")]
$group.bench_function("OpenSSL", |b| {
use openssl::symm::Cipher;
b.iter(|| {
use openssl::symm::{decrypt, encrypt};
let data = encrypt($openssl, &$key, Some(&$iv), &$data).unwrap();
let _ = decrypt($openssl, &$key, Some(&$iv), data.as_ref()).unwrap();
})
});
}};
}

macro_rules! benchmark_padded {
($fn:ident, $test:literal, $file:literal, $awslc:expr, $mode:expr, $openssl:expr) => {
fn $fn(c: &mut Criterion) {
test::run(test_file!($file), |_section, test_case| {
let key_bytes = test_case.consume_bytes("KEY");
let iv = test_case.consume_bytes("IV");
let data = test_case.consume_bytes("IN");

let mut group = c.benchmark_group(format!("{}-{}-bytes", $test, data.len()));

group.bench_function("AWS-LC", |b| {
b.iter(|| {
let key = UnboundCipherKey::new($awslc, &key_bytes).unwrap();
let iv: CipherContext =
CipherContext::Iv128(iv.as_slice().try_into().unwrap());

let encrypt_key = match $mode {
OperatingMode::CBC => PaddedBlockEncryptingKey::cbc_pkcs7(key),
_ => unreachable!(),
}
.unwrap();

let mut in_out = Vec::from(data.as_slice());
let context = encrypt_key.less_safe_encrypt(&mut in_out, iv).unwrap();

let key = UnboundCipherKey::new($awslc, &key_bytes).unwrap();

let decrypt_key = match $mode {
OperatingMode::CBC => PaddedBlockDecryptingKey::cbc_pkcs7(key),
_ => unreachable!(),
}
.unwrap();

let _ = decrypt_key.decrypt(&mut in_out, context).unwrap();
})
});

openssl_bench!(group, $openssl, key_bytes, iv, data);

Ok(())
});
}
};
}

macro_rules! benchmark_unpadded {
($fn:ident, $test:literal, $file:literal, $awslc:expr, $mode:expr, $openssl:expr) => {
fn $fn(c: &mut Criterion) {
test::run(test_file!($file), |_section, test_case| {
let key_bytes = test_case.consume_bytes("KEY");
let iv = test_case.consume_bytes("IV");
let data = test_case.consume_bytes("IN");

let mut group = c.benchmark_group(format!("{}-{}-bytes", $test, data.len()));

group.bench_function("AWS-LC", |b| {
b.iter(|| {
let key = UnboundCipherKey::new($awslc, &key_bytes).unwrap();
let iv: CipherContext =
CipherContext::Iv128(iv.as_slice().try_into().unwrap());

let encrypt_key = match $mode {
OperatingMode::CTR => EncryptingKey::ctr(key),
_ => unreachable!(),
}
.unwrap();

let mut in_out = Vec::from(data.as_slice());
let context = encrypt_key.less_safe_encrypt(&mut in_out, iv).unwrap();

let key = UnboundCipherKey::new($awslc, &key_bytes).unwrap();

let decrypt_key = match $mode {
OperatingMode::CTR => DecryptingKey::ctr(key),
_ => unreachable!(),
}
.unwrap();

let _ = decrypt_key.decrypt(&mut in_out, context).unwrap();
})
});

openssl_bench!(group, $openssl, key_bytes, iv, data);

Ok(())
});
}
};
}

benchmark_unpadded!(
test_aes_128_ctr,
"AES-128-CTR",
"data/cipher_aes_128_ctr.txt",
&AES_128,
OperatingMode::CTR,
Cipher::aes_128_ctr()
);

benchmark_unpadded!(
test_aes_256_ctr,
"AES-256-CTR",
"data/cipher_aes_256_ctr.txt",
&AES_256,
OperatingMode::CTR,
Cipher::aes_256_ctr()
);

benchmark_padded!(
test_aes_128_cbc,
"AES-128-CBC",
"data/cipher_aes_128_cbc.txt",
&AES_128,
OperatingMode::CBC,
Cipher::aes_128_cbc()
);

benchmark_padded!(
test_aes_256_cbc,
"AES-256-CBC",
"data/cipher_aes_256_cbc.txt",
&AES_256,
OperatingMode::CBC,
Cipher::aes_256_cbc()
);

criterion_group!(
benches,
test_aes_128_ctr,
test_aes_128_cbc,
test_aes_256_ctr,
test_aes_256_cbc
);
criterion_main!(benches);
14 changes: 14 additions & 0 deletions aws-lc-rs/benches/data/cipher_aes_128_cbc.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 1 block
KEY = d1ae485dbe0d98ae4eff24ae075a5d28
IV = a7cbf70ede88eb876887c9e7ed60a108
IN = 6d00bced19251c9d4c9a8ee7ead11881

# 16 blocks
KEY = 78f094594b7ee4194bc2baed0562856e
IV = 5591c315865d3b312292be1c8b3babdf
IN = 98b4eae2434eb203583d837f90b17d92c5aaf51cb6d160566a691d1e254067e1a038416723ab31036775e560d6c9d692a1d083dc66b348f1a47fd69c5a890044345d9c0acad8db746280b42fdd17cd07a2c7df684d979d06eb41df6b5dabf1ff6d64ad54462966dfbb1dc8d3d19085b9f9b85f2892ddce92f0b4da4fd9d97d60d9ef171f27a895bc2e00aaa7532a1230536998f246e005688e698eb7edecf05bfda05b93ac63f7eabff0296d9442a0a0c3942985b86e2b5f3a38df65be6fc8ee690ecadab6acc3b75bc4580f54101bcedf6c131081faab8c3e8a322e252260dada51e63dfe470d0d0199d2e9c2f50a77a48d382e3986c26d0db8915ef2e25e28

# 256 blocks
KEY = 4da3482c72e5eec455e919054118103a
IV = a4a809574a1a9575b4256cc704a53e46
IN = a606b6054c433bf7b052aa96b5438ced6675050987c7f9036bb9bc261e5c422c05b155c85bf319f31cef15370f8adb6b574484bfa61cbc203b2875c9d86fae358ac3a59db1230f0e614d68df10ad53077da9b609996f7fdcca9ccc3ca32c1474e563cbb6c97d8113b5f6591f72ac42ac9cdf76d4660a29b5d9b2c8157bd83ce02eb895a8b300a61f886c5d4ca7d07a7166cbd0b9d2ad818e0b1e6ae86ce17b15b67a468446268bf23f61f4fab478d1dee9443ba82b981b5f133561a3019da21b86d848530e6bfaa874d8866cab995ab7a944acac71bb1b7e398dd94a7d69f25f692a2824dcb2354e846d561d4a13d89cd69cd148e4e14d4169da0334b17664b7910f3ad5ab71ba45549fcfdad84da05795b15d6256c99110a5585584230b343ed08990a8b3033da1a07bc47de455548456b4b95f78761482811cbdf39ea35231eebf1f0792667302a269ac534d4a52b1b1927c245b566c9181002db0f6eaf22293196713e514311a658dc616e2ec4f9d8790368e627cf0b741be2a346700ae5456496464162e496ebbc19801059c9c53da709cc74687ecd5fa3d3ef740642f8740a630ff65f9b7b391c6def821e0f01e3c70503846e15e1d53204978a7d85a4eb8c3b654e3d3a7719b8661396235a3455a52de6f3e29c3333dd430eb166ad835d26cfa3be41bd3c3cafdd2bf1956447bbec205cf254f906569fd8bcf5ebaa3d3445f21938b3314e132fbbbb796a3e9abd393277650d5061833e12d6f009baf6543e0bab99be09b39975caff634c5988263e95e1861231b54228d93b3709ea17e29f0614d77da6578b9807b0e8f603f81417ddfe64936b5a337bf8e6055658d58a8680d07d9439fd453274d58dcd4cf6a5eb6a21e96dbdec9d534ee803f3ad830e58d18836ea51964098acd9af4bade9c473387288ce7ef9a73b843e272904e1bd1629e941be8590bb0c254202f93035a9d1708214a5fa0c66cce0f7db2084f63c06de42aab893a587af9b232d2dbdbbc1c85e7005845ad6de9b04ec002743392a4a6282132da9209bd5df5adf4358ac914f83186a58386629c6b1e3032ce2be4454ba7cbff15bc340fd14a51da3b54cd35ea1593b2cabd32004f45f5ae4ec6c7ed79d3d9c512d7299ccf4466958bf379211dffb90b2d8f3737dcf358f659880184707cacbff2184c39134d874fc85c21aabe6fab711f18170b0e5fd38838e19c64b77a6709612f4cd8ada37d31b5ec01347e3b7f777d0a4eb0b7f70094f6856b5bee20118cbd982d6fbf90831b73a4eeabade1243cc52a440f735daf53a6eb12d354faba5938e7e41c6c9d79bbb63d44e1b766659cf7f20d9b2b45fe66ca8fc294f191a0e5ecf7c189afab57687bc599534d5bdcc001c145d0a852e0954d5318e76350dea1afc84a274faad3eb24cafb70e5073fe214a83c8251be1a9e106d86149f569a9ef9aa1f2d473bcb06072a6cb994f400175c788b431f44f7e70f0d7563a0850111f9835b3553d138436e60e2f45a1d147bc6c111447faa79001b05b70f11919ddb1a5681a74c8e08aa892d5a1af635305e18d67e116d26662ca92072be5b9a85cd9da0b97d47f2019b12daa6a5b03f18569857a42e139a5c59cb8b876b0a6965383b4b067d2eb9d3d382b5f96c05d1747eeb39c42815f028698bb94b45fbf501104bb5127ac9213c505460e8598a099bdf2ba9f6c7afca91d34125d642a1fc9746b685ea3adcb7ad49b9ceafda3cef3f0254a28c4000611780946c5de2255af12da37e983c2b41b92ddffd59de694aa79ddc18f49f56ec43bc138c21095d9b6cd82607679a386902957c240c8e4d9196030c2798685bc78b4abbcb823eaeed99995b009bd45c2d667c7652f0d65cf781a1ae5d7348412436f78410fd0b6caf9a80caeb3e9ca2a11345399b37111cd0c579cd24ddf86652634ad6cb1b2fda3e1a194f695e2d1c3247cee2b8a88e6d373d3ac6d29b594306b749ae74b0507e4c70e29536e0a6dc5b5e85c25936117b3e6d0eee31c6685b2e45d152f74de49f82820338653dd2bd43224bfe5f0c94247a1c8e13a03bc0b9eaec114789ca4e4939fd267d261377cafe12e5adadb5df05be0555c11a78b38e454bcbf5dfe263aade057224c0aa7632a4a4ca17f9c97d983d261204a798dcf651d60e0b93f3b4337aa06c793c812fa088ebc9b07df4ca79a3105a33d3b3706c468f258c2a6b7b4a2f4fa1d589eb795c4a0b325b963cbc99eb14a23b5657f9717a6608631bbff1b94337d0c66fe7cdb391476d7c14180b538d83df38086640ff48b1e3f4c7048cef1970df7c43af5c9d586d0cdef4cf51f96aa4539b7d9608261863949e57781b6148b4892a1f42e0169e1486428a8ef5bdb0bf1a00e27c79ae2740e601c54bf5e7c9c96f82a7df1f437ea1d423170617c02751959cb25bbe310ef28ee26718580f85a454e2fb35215b3c52422f69109549c49aa3e4b4e126f2a8a38cc79c4aee2e900cc5073d1a590ada05cdd4a84c6c56247fedb47efe1b8aa33bce4e616623d7e1b250c242e6cd763250b1623db116fce701f46ddb7c9d63bf3ddb173961ae354194c3d2674a228ae977f7de78f7517e0e5612272b6dfc627dfa01f7cc0f4d6c3b5d591bb3c80f77eb23e9d2385666efe0c7fc98ce3fa02c24bc6209bd6db3ba5976d443ff106a41827b156a37a7afc62c916972a410a0ad7cb33188e7db331ec7fb51bdc1fb5537aedc0d1476e1d71adf060439f57a1b83a5af191b0e12ca533b0348e997dce20798fdd2d01c5424c7d606b435c8141578210447d6dec63b06739722a138f0fd2bcdf6cc455b175b07fe0e96c7003662ad71b047884205ce1a83e9a96b9acfa2d837a1b17b4ad865b778646517a33a9515b09b7659f932d7cc660eb5bc39805ed8b1831d72365d772f7875259da722a18274010483df24868c8347cc1dcc67d25fa148a355c0bcdeac84f5aad0af892b24df2928059508c98003ed734ab6eeaeac67060e992c2c1f991247439b514ca3ebe038a4adf04d5d6514e17bbd9ae58c160eaa2a8606d1f9e4dc6e03f48c2c94230f602cfb2c5b285d5ac0d00bdd8392302cfce89fca520e2c91f6b870028a2850b8da2ef86c1088a61a3780bc0f3eca4b28bf84842e455c320b256c26c2b89a03cb9e73821a1014008a175793d35f60641c972352bd3851ec88246960a18672a68dc062d97009f3d34e194c32c124b22e966b63d199e102508b6e23e9d7b0ea38e5562a4198688162219bd70a1b4eb2f77077e6d1b5cc4556a7b9825cd4528e9f96fb7fc65b247baabe903c9de016642f5cd71de47f9c605e985973a3bb0699f54406e66606836d9d7c8b707c8964b59a0f963e0d5f6cc23c05f52b49118d8fcb4e488381104435583ce71583b285c02e4ceff41c81a7b9639d4d0d40dc1f4fca3ff58406426a1aa9a026bd258b6e0f103f1a0850d289b3126afaf17cdaaf5ace15c735363155f4b1c869d6421d015ad499bc9081891686542fb9ddb6f58c3c69e23ace91d75685e93161bf80ff3cb4f3dc946d94b4cece9ececbc9a20975543c7f358346a028d774c811d72ae68da1fad92e6cc6aa497c6365f2003f8e4754faafe1a7b48e626e5613ffe880c404ad0a8104186ccc5c4392f94316f437db2d0bfe4c68394ce764377e810c8a3c9b938c8f4b7727fd1d067f3b38490663ca3183c453cbe760cb0a92d236dd153f51f45b2193437b3340f8a6a44ff30dfbb7e3a57dec7cd80b3288797147e95bfb821c84a7401595f0521c20337a9e9e31714b0b4b7c4708604db81ae0b5755b6c04491d81023ac7f6a32b8f92e46cc339922f381dd6e475d0a43c03707fd2b5796e272d2977569e1ecf43fdc98a41dcd017e10abd0b9ce75ca0fcf3d01b777c765dc6763cfee650802e1c50f04f9995b0b742100fcb6bb955e27974607877207c48df18ae3dc424900c30ac2ac84c7c89594211a3117d53f68c39f7456eb51cace912c7c592580e087bffd86d6c80e89d9d605bbbc29f4197b46136d04ca4d78415b308ec3800867b6abff0bc610a75295c3833e6eb6cd7de5b40e21d083228a9eb0293809061ab63810d035904f75575bcc3c229e37c6fbdde651f3a77bb99998ac6234cfdfd04b6e0e90082387798fff8497de30e66183cd545ac1964857ee1bd277d8594fec991ca9429abc0393c307187dd41e3eb9bffce0b6882c85de6d68b2638b1061c534effd25f7bc702ba2e109fa78ff5e45e308e2e45df1b9846fe3694ee7e2a7c1cb82b9d2b0f0b2e86c69acad5b98d30df79444445eb1143605110219ebf6f203626632d426b1583f83101ba1abcabe233b5409c1b48f20e6453a66a6cf15361679ae99aac1971559ecf58896f5c950ac6b7482784872665d3ce563c89dec404332dc32c19eb69093d556341d7ec2beac4a70ee6a07023b0e8ca7dd945199d46acf05c476b78fe4883b9a193294b3dbf5b0b8d143d7a7682d016ad4e6741eef91de74565c2c57030d86c598b271041cdb8bb5125cf43f9d64f83774f3b4032c0accf6c2787a4599011d4b9ba848b50fb47da918f6800de5055b3a566fb858dbd2f9d6db758d801ce2494ce22ec824e74300b030014ba3c54f0b8544d737e7761f9c2aced8114e5a92827928e9c000dd4353b92fc61f32d172b9133a5c3386adb3030c159b252b01163b2093830f7487d095baf7aacb2f135aee40e04d866d07c97ff273af7251cb6e8aedeca0f5e06f9776464ff8a0bf95aa0033c597f3cb4144ebd2b6dc5b1114e544cd22d445b4cb504fdfd7a392e5c300eb9c463c027f5e48e8e81dd819ada10ce2aa1c3c37c70ad5970289c3204274bf8762611041bf9c5b6864bbf5fc8d31b0772626bfe1b1f5f634bdaf36f1652d5687a69b21e4010b3e5d82f31c63e07904b104b87809d7e2f52f58c3008ae8fa98fae65e8e7609671e89a3e1bb4a0eab52b5e541863bc2fa850b855f4ecc7e08e5a9293bfb48ba6f03f5042816234b304c6f2af3df3029123f44ea52e99d2b59f3211a16096aa03cd8e7fcdc277665f32cac3f023a1468cd2aea4e8863c63dc0af27b3a42a071f4e16970ee06c1fc287df2401e0f2d719ab8e217efeb114c2c0bf18c41933f898245395e77961815ed05e9366d20b2083a5b681ff823609b1b67d1cdcb9ab11b5e80b971ade516186b4d1fe4e4528a35d790e96d857bf78658eda334140ed19f5599b474dbc7f3934bf5459dbdfff6b201a7cff6ddef2d621b30e6e5ba532a1dd3b8b68540f94b32e9ac7b40ef06804147e31e2d7c5c3344a2fcf9ccd4726d4484f05966a4ddbc6ee717d9d71b1f33afc7afa5fdf2234297c4b8db3e9f4de2a884bf91a534066f4ad988d6c498a09abd95b5ffbafe5119953878bc6611f56fa26662802ee6dcd759a17bd9b0c4391ff7ad8eccf7bfba5a97450d179df34116b5c7c85a6bbd6eb61b596227c968e3118640954e17fb01455bfee4051d4b6ee2c01c3f5fe4a4acf965c94f5aa9a699349f4ebec88ebfe1deeb8bbc512f2d11609c52eede49ca6313b192110230d0b58624d1d05a28248bd29acedf348c67a3c686892a1b2f167731e164eebad6f3a3216af54c201daca26a6027b0e7d3c05bf00f26091b451e17489f1c05b475158a14071d0103c692f8fb6c4621077fbe591da55f648a35fa359e993499fdb135c466e6994ab954fc773186de49b20bef7a1b6cb8954d49e22ba1d27103677faca2f9f8b19b92dd4169b564ddb7ce90fc66acc113418ff84d36382f3fe01a92fe54c54093d0386b67
Loading