///|
let block_key : Bytes = Bytes::from_array(
[0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6].map(v => v.to_byte()),
)
///|
let verifier_hash_input_block_key : Bytes = Bytes::from_array(
[0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79].map(v => v.to_byte()),
)
///|
let verifier_hash_value_block_key : Bytes = Bytes::from_array(
[0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e].map(v => v.to_byte()),
)
///|
let iter_count = 50_000
///|
let package_encryption_chunk_size = 4096
///|
let package_offset = 8
///|
let max_password_length = 255
///|
priv struct KeyData {
salt_size : Int
block_size : Int
key_bits : Int
hash_size : Int
cipher_algorithm : String
cipher_chaining : String
hash_algorithm : String
salt_value : Bytes
}
///|
priv struct DataIntegrity {
encrypted_hmac_key : Bytes
encrypted_hmac_value : Bytes
}
///|
priv struct EncryptedKey {
spin_count : Int
key_data : KeyData
encrypted_verifier_hash_input : Bytes
encrypted_verifier_hash_value : Bytes
encrypted_key_value : Bytes
}
///|
priv struct EncryptionInfo {
key_data : KeyData
data_integrity : DataIntegrity?
encrypted_key : EncryptedKey
}
///|
priv enum EncryptionMechanism {
Agile
Standard
}
///|
priv struct DecryptedPackage {
bytes : Bytes
password_verified : Bool
}
///|
priv struct ByteWriter {
buf : Array[Byte]
}
///|
fn ByteWriter::new() -> ByteWriter {
{ buf: [] }
}
///|
fn ByteWriter::write_bytes(self : ByteWriter, value : BytesView) -> Unit {
self.buf.append(value.to_array())
}
///|
fn ByteWriter::write_u16_le(self : ByteWriter, value : Int) -> Unit {
self.buf.push((value & 0xff).to_byte())
self.buf.push(((value >> 8) & 0xff).to_byte())
}
///|
fn ByteWriter::write_u32_le(self : ByteWriter, value : Int) -> Unit {
let v = value.reinterpret_as_uint()
@crypto.push_u32_le(self.buf, v)
}
///|
fn ByteWriter::write_u64_le(self : ByteWriter, value : Int) -> Unit {
let v = value.to_uint64()
for i in 0..<=7 {
self.buf.push(((v >> (8 * i)) & 0xff).to_byte())
}
}
///|
fn ByteWriter::write_utf16le(self : ByteWriter, value : StringView) -> Unit {
let bytes = @encoding/utf16.encode(value, bom=false, endianness=Little)
self.write_bytes(bytes)
}
///|
fn ByteWriter::to_bytes(self : ByteWriter) -> Bytes {
Bytes::from_array(self.buf)
}
///|
fn normalize_hash_algorithm(name : StringView) -> String {
let lower = name.to_lower().to_owned()
if lower.has_prefix("sha-") {
lower.replace_all(old="-", new="")
} else {
lower
}
}
///|
fn hash_parts(
algorithm : String,
parts : ArrayView[BytesView],
) -> Bytes raise XlsxError {
let normalized = normalize_hash_algorithm(algorithm)
let out = @crypto.hash_concat(normalized, parts)
if out.length() == 0 {
raise UnsupportedEncryption(msg="unsupported hash algorithm: \{algorithm}")
}
out
}
///|
fn aes_ecb_encrypt(key : BytesView, data : BytesView) -> Bytes raise XlsxError {
@crypto.aes_ecb_encrypt(key, data) catch {
InvalidCrypto(msg~) => raise InvalidEncryptionInfo(msg~)
}
}
///|
fn aes_ecb_decrypt(key : BytesView, data : BytesView) -> Bytes raise XlsxError {
@crypto.aes_ecb_decrypt(key, data) catch {
InvalidCrypto(msg~) => raise InvalidEncryptionInfo(msg~)
}
}
///|
fn aes_cbc_decrypt(
key : BytesView,
iv : BytesView,
data : BytesView,
) -> Bytes raise XlsxError {
@crypto.aes_cbc_decrypt(key, iv, data) catch {
InvalidCrypto(msg~) => raise InvalidEncryptionInfo(msg~)
}
}
///|
fn aes_ecb_decrypt_cancellable(
key : BytesView,
data : BytesView,
cancelled : () -> Bool,
) -> Bytes raise XlsxError {
if data.length() % 16 != 0 {
raise InvalidEncryptionInfo(msg="invalid AES data length")
}
let out : Array[Byte] = Array::new(capacity=data.length())
let mut offset = 0
while offset < data.length() {
check_read_cancelled(cancelled)
let end = (offset + 4096).min(data.length())
out.append(aes_ecb_decrypt(key, data[offset:end]).to_array())
offset = end
}
check_read_cancelled(cancelled)
Bytes::from_array(out)
}
///|
fn byte_xor(a : Byte, b : Byte) -> Byte {
(a.to_int() ^ b.to_int()).to_byte()
}
///|
fn u32_le_bytes(value : Int) -> Bytes {
let out : Array[Byte] = []
@crypto.push_u32_le(out, value.reinterpret_as_uint())
Bytes::from_array(out)
}
///|
fn u64_le_bytes(value : UInt64) -> Bytes {
let out : Array[Byte] = []
for i in 0..<=7 {
out.push(((value >> (8 * i)) & 0xff).to_byte())
}
Bytes::from_array(out)
}
///|
fn pad_zero(data : BytesView, block_size : Int) -> Bytes {
let len = data.length()
let remainder = len % block_size
if remainder == 0 {
return data.to_owned()
}
let padded_len = len + (block_size - remainder)
let out : Array[Byte] = data.to_array()
while out.length() < padded_len {
out.push((0).to_byte())
}
Bytes::from_array(out)
}
///|
/// Lazily-initialized process-global generator behind `random_bytes`.
/// A fresh `Rand::new()` per call would reuse ChaCha8's fixed default
/// seed, making every salt/verifier identical across files and runs.
let random_bytes_rng : Ref[@random.Rand?] = { val: None }
///|
/// Optional user-supplied source of cryptographically secure bytes.
let random_bytes_override : Ref[((Int) -> Bytes)?] = { val: None }
///|
/// Installs a custom source for the random bytes used in workbook
/// encryption and sheet-protection salts, verifiers, and keys. The
/// function receives the byte count required and must return exactly
/// that many bytes.
///
/// Use this to supply cryptographically secure entropy (Go excelize
/// uses crypto/rand for these values). The built-in fallback is a
/// ChaCha8 generator seeded from wall-clock entropy whitened through
/// SHA-256: best-effort unique per process (millisecond clock
/// resolution means simultaneous process starts can collide), and not
/// a CSPRNG.
pub fn set_random_source(source : (Int) -> Bytes) -> Unit {
random_bytes_override.val = Some(source)
}
///|
fn seeded_random_rng() -> @random.Rand {
match random_bytes_rng.val {
Some(rng) => rng
None => {
let tag : Bytes = b"mbtexcel-random-bytes-seed"
let first = @crypto.hash_concat("sha256", [u64_le_bytes(@env.now()), tag])
let seed = @crypto.hash_concat("sha256", [first, u64_le_bytes(@env.now())])
let rng = @random.Rand::chacha8(seed~)
random_bytes_rng.val = Some(rng)
rng
}
}
}
///|
fn random_bytes(count : Int) -> Bytes {
match random_bytes_override.val {
Some(source) => {
let bytes = source(count)
if bytes.length() != count {
abort("random source returned \{bytes.length()} bytes, need \{count}")
}
return bytes
}
None => ()
}
let rng = seeded_random_rng()
let out : Array[Byte] = Array::make(count, (0).to_byte())
for i in 0.. String raise XlsxError {
match attr_value(tag, name) {
Some(value) => value
None => raise InvalidEncryptionInfo(msg="missing \{name} attribute")
}
}
///|
fn parse_int_attr(tag : StringView, name : String) -> Int raise XlsxError {
let text = parse_required_attr(tag, name)
@string.parse_int(text, base=10) catch {
_ => raise InvalidEncryptionInfo(msg="invalid \{name} value")
}
}
///|
fn parse_bytes_attr(tag : StringView, name : String) -> Bytes raise XlsxError {
let text = parse_required_attr(tag, name)
base64_decode(text)
}
///|
fn tag_attributes(
xml : StringView,
tag_name : StringView,
) -> String? raise XlsxError {
let xml_str = xml.to_owned()
let needle = "<" + tag_name.to_owned()
match xml_str.find(needle) {
None => None
Some(start) => {
let rest = xml_str[start + needle.length():]
let end = match rest.find(">") {
Some(pos) => pos
None => raise InvalidEncryptionInfo(msg="tag not closed")
}
let tag = rest[:end]
Some(tag.to_owned())
}
}
}
///|
fn parse_key_data(tag : StringView) -> KeyData raise XlsxError {
{
salt_size: parse_int_attr(tag, "saltSize"),
block_size: parse_int_attr(tag, "blockSize"),
key_bits: parse_int_attr(tag, "keyBits"),
hash_size: parse_int_attr(tag, "hashSize"),
cipher_algorithm: parse_required_attr(tag, "cipherAlgorithm"),
cipher_chaining: parse_required_attr(tag, "cipherChaining"),
hash_algorithm: parse_required_attr(tag, "hashAlgorithm"),
salt_value: parse_bytes_attr(tag, "saltValue"),
}
}
///|
fn parse_data_integrity(tag : StringView) -> DataIntegrity raise XlsxError {
{
encrypted_hmac_key: parse_bytes_attr(tag, "encryptedHmacKey"),
encrypted_hmac_value: parse_bytes_attr(tag, "encryptedHmacValue"),
}
}
///|
fn parse_encrypted_key(tag : StringView) -> EncryptedKey raise XlsxError {
{
spin_count: parse_int_attr(tag, "spinCount"),
key_data: parse_key_data(tag),
encrypted_verifier_hash_input: parse_bytes_attr(
tag, "encryptedVerifierHashInput",
),
encrypted_verifier_hash_value: parse_bytes_attr(
tag, "encryptedVerifierHashValue",
),
encrypted_key_value: parse_bytes_attr(tag, "encryptedKeyValue"),
}
}
///|
fn parse_encryption_info(xml : StringView) -> EncryptionInfo raise XlsxError {
let key_data_tag = match tag_attributes(xml, "keyData") {
Some(value) => value
None => raise InvalidEncryptionInfo(msg="keyData tag missing")
}
let encrypted_key_tag = match tag_attributes(xml, "p:encryptedKey") {
Some(value) => value
None =>
match tag_attributes(xml, "encryptedKey") {
Some(value) => value
None => raise InvalidEncryptionInfo(msg="encryptedKey tag missing")
}
}
let data_integrity_tag = tag_attributes(xml, "dataIntegrity")
{
key_data: parse_key_data(key_data_tag),
data_integrity: match data_integrity_tag {
Some(value) => Some(parse_data_integrity(value))
None => None
},
encrypted_key: parse_encrypted_key(encrypted_key_tag),
}
}
///|
fn validate_key_data(key_data : KeyData) -> Unit raise XlsxError {
if key_data.salt_value.length() != key_data.salt_size {
raise InvalidEncryptionInfo(msg="salt size mismatch")
}
let hash = hash_parts(key_data.hash_algorithm, [b""])
if hash.length() != key_data.hash_size {
raise InvalidEncryptionInfo(msg="hash size mismatch")
}
if key_data.key_bits != 128 &&
key_data.key_bits != 192 &&
key_data.key_bits != 256 {
raise InvalidEncryptionInfo(msg="invalid AES key size")
}
}
///|
fn validate_encryption_info(
encryption : EncryptionInfo,
limits? : ReadLimits = ReadLimits::new(),
) -> Unit raise XlsxError {
validate_key_data(encryption.key_data)
validate_key_data(encryption.encrypted_key.key_data)
if encryption.encrypted_key.spin_count < 0 {
raise InvalidEncryptionInfo(msg="negative Agile KDF work factor")
}
if encryption.encrypted_key.spin_count > limits.max_kdf_iterations {
raise ResourceLimitExceeded(
kind="kdf_iterations",
limit=limits.max_kdf_iterations,
actual=encryption.encrypted_key.spin_count,
)
}
if encryption.encrypted_key.encrypted_verifier_hash_input.length() == 0 ||
encryption.encrypted_key.encrypted_verifier_hash_value.length() == 0 {
raise InvalidEncryptionInfo(msg="verifier data missing")
}
let block_size = encryption.encrypted_key.key_data.block_size
if block_size <= 0 ||
encryption.encrypted_key.encrypted_verifier_hash_input.length() % block_size !=
0 ||
encryption.encrypted_key.encrypted_verifier_hash_value.length() % block_size !=
0 ||
encryption.encrypted_key.encrypted_key_value.length() == 0 ||
encryption.encrypted_key.encrypted_key_value.length() % block_size != 0 {
raise InvalidEncryptionInfo(msg="invalid encrypted verifier data")
}
match encryption.data_integrity {
Some(value) =>
if value.encrypted_hmac_key.length() == 0 ||
value.encrypted_hmac_value.length() == 0 {
raise InvalidEncryptionInfo(msg="hmac data missing")
}
None => ()
}
}
///|
fn is_aes_cipher(key_data : KeyData) -> Bool {
key_data.cipher_algorithm.to_lower() == "aes" &&
key_data.cipher_chaining.to_lower().contains("cbc") &&
key_data.block_size == 16 &&
(
key_data.key_bits == 128 ||
key_data.key_bits == 192 ||
key_data.key_bits == 256
)
}
///|
fn encryption_mechanism(
info : BytesView,
) -> EncryptionMechanism raise XlsxError {
if info.length() < 4 {
raise InvalidEncryptionInfo(msg="invalid encryption info")
}
let major = read_u16_le(info, 0)
let minor = read_u16_le(info, 2)
if major == 4 && minor == 4 {
Agile
} else if major >= 2 && major <= 4 && minor == 2 {
Standard
} else if (major == 3 || major == 4) && minor == 3 {
raise UnsupportedEncryption(msg="unsupported extensible encryption")
} else {
raise UnsupportedEncryption(msg="unknown encryption mechanism")
}
}
///|
fn derive_password_hash(
password : String,
encryption : EncryptionInfo,
cancelled? : () -> Bool = () => false,
) -> Bytes raise XlsxError {
let key_data = encryption.encrypted_key.key_data
let salt_value = key_data.salt_value
let password_bytes = @encoding/utf16.encode(
password,
bom=false,
endianness=Little,
)
let mut key = hash_parts(key_data.hash_algorithm, [salt_value, password_bytes])
for i in 0.. Bytes raise XlsxError {
let key_data = encryption.encrypted_key.key_data
let key = hash_parts(key_data.hash_algorithm, [password_hash, block])
let key_bytes = key_data.key_bits / 8
if key.length() < key_bytes {
let out : Array[Byte] = key.to_array()
while out.length() < key_bytes {
out.push((0x36).to_byte())
}
Bytes::from_array(out)
} else if key.length() > key_bytes {
key[:key_bytes].to_owned()
} else {
key
}
}
///|
fn agile_verify_password(
password_hash : BytesView,
encryption : EncryptionInfo,
) -> Bool raise XlsxError {
let key_data = encryption.encrypted_key.key_data
let salt = key_data.salt_value
let input_key = derive_password_key_from_hash(
password_hash, verifier_hash_input_block_key, encryption,
)
let value_key = derive_password_key_from_hash(
password_hash, verifier_hash_value_block_key, encryption,
)
let verifier_input = aes_cbc_decrypt(
input_key,
salt,
encryption.encrypted_key.encrypted_verifier_hash_input,
)
let verifier_hash_value = aes_cbc_decrypt(
value_key,
salt,
encryption.encrypted_key.encrypted_verifier_hash_value,
)
let expected_hash = hash_parts(key_data.hash_algorithm, [verifier_input])
let hash_size = key_data.hash_size
let size = if hash_size < expected_hash.length() {
hash_size
} else {
expected_hash.length()
}
if verifier_hash_value.length() < size {
return false
}
expected_hash[:size].equal(verifier_hash_value[:size])
}
///|
fn create_iv_from_block(
block : BytesView,
encryption : EncryptionInfo,
) -> Bytes raise XlsxError {
let salt = encryption.key_data.salt_value
let mut iv = hash_parts(encryption.key_data.hash_algorithm, [salt, block])
let block_size = encryption.key_data.block_size
if iv.length() < block_size {
let out : Array[Byte] = iv.to_array()
while out.length() < block_size {
out.push((0x36).to_byte())
}
iv = Bytes::from_array(out)
} else if iv.length() > block_size {
iv = iv[:block_size].to_owned()
}
iv
}
///|
fn create_iv(
block_index : Int,
encryption : EncryptionInfo,
) -> Bytes raise XlsxError {
let block_bytes = u32_le_bytes(block_index)
create_iv_from_block(block_bytes, encryption)
}
///|
fn decrypt_package(
package_key : BytesView,
input : BytesView,
encryption : EncryptionInfo,
cancelled? : () -> Bool = () => false,
) -> Bytes raise XlsxError {
check_read_cancelled(cancelled)
let block_size = encryption.key_data.block_size
if input.length() <= package_offset ||
(input.length() - package_offset) % block_size != 0 {
raise InvalidEncryptedPackage(msg="invalid encrypted package length")
}
let payload = input[package_offset:]
let out : Array[Byte] = []
let mut offset = 0
let mut index = 0
while offset < payload.length() {
check_read_cancelled(cancelled)
let end = if offset + package_encryption_chunk_size < payload.length() {
offset + package_encryption_chunk_size
} else {
payload.length()
}
let chunk = payload[offset:end]
let padded = pad_zero(chunk, block_size)
let iv = create_iv(index, encryption)
let decrypted = aes_cbc_decrypt(package_key, iv, padded)
out.append(decrypted.to_array())
offset = end
index = index + 1
}
check_read_cancelled(cancelled)
let raw_size_u64 = @crypto.u64_from_le(input, 0)
if raw_size_u64 > out.length().to_uint64() {
raise InvalidEncryptedPackage(msg="decrypted package size is invalid")
}
out.truncate(raw_size_u64.to_int())
Bytes::from_array(out)
}
///|
fn agile_decrypt(
encryption_info_buf : BytesView,
encrypted_package_buf : BytesView,
password : String,
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> DecryptedPackage raise XlsxError {
check_read_cancelled(cancelled)
if encryption_info_buf.length() > limits.max_xml_part_bytes {
raise ResourceLimitExceeded(
kind="xml_part_bytes",
limit=limits.max_xml_part_bytes,
actual=encryption_info_buf.length(),
)
}
let xml = @encoding/utf8.decode(encryption_info_buf) catch {
_ => raise InvalidEncryptionInfo(msg="invalid encryption info xml")
}
let encryption = parse_encryption_info(xml)
validate_encryption_info(encryption, limits~)
if !is_aes_cipher(encryption.key_data) ||
!is_aes_cipher(encryption.encrypted_key.key_data) {
raise UnsupportedEncryption(msg="unsupported cipher settings")
}
let password_hash = derive_password_hash(password, encryption, cancelled~)
let key = derive_password_key_from_hash(password_hash, block_key, encryption)
let password_ok = agile_verify_password(password_hash, encryption)
let salt = encryption.encrypted_key.key_data.salt_value
let package_key = aes_cbc_decrypt(
key,
salt,
encryption.encrypted_key.encrypted_key_value,
)
let decrypted = decrypt_package(
package_key,
encrypted_package_buf,
encryption,
cancelled~,
)
{ bytes: decrypted, password_verified: password_ok }
}
///|
fn standard_convert_password_to_key(
key_bits : Int,
salt : BytesView,
password : String,
cancelled? : () -> Bool = () => false,
) -> Bytes raise XlsxError {
let password_bytes = @encoding/utf16.encode(
password,
bom=false,
endianness=Little,
)
let mut key = hash_parts("sha1", [salt, password_bytes])
for i in 0.. Bool raise XlsxError {
if algorithm != "AES" {
// Legacy RC4-style headers do not use the AES verifier layout. Their
// password classification falls back to the single bounded ZIP parse.
return false
}
let hash_size_u64 = read_u32_le(verifier_block, 36).to_uint64()
// Standard AES always verifies the complete SHA-1 digest. Accepting a
// caller-declared prefix would make password verification probabilistic.
if hash_size_u64 != 20UL {
raise InvalidEncryptionInfo(msg="invalid verifier hash size")
}
let hash_size = hash_size_u64.to_int()
let encrypted_hash_size = (hash_size + 15) / 16 * 16
if verifier_block.length() < 40 + encrypted_hash_size {
raise InvalidEncryptionInfo(msg="invalid verifier block")
}
let verifier = aes_ecb_decrypt(secret_key, verifier_block[20:36])
let verifier_hash = aes_ecb_decrypt(
secret_key,
verifier_block[40:40 + encrypted_hash_size],
)
let expected = hash_parts("sha1", [verifier])
expected[:hash_size].equal(verifier_hash[:hash_size])
}
///|
fn standard_decrypt(
encryption_info_buf : BytesView,
encrypted_package_buf : BytesView,
password : String,
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
) -> DecryptedPackage raise XlsxError {
check_read_cancelled(cancelled)
if encryption_info_buf.length() < 12 {
raise InvalidEncryptionInfo(msg="standard encryption header too short")
}
let header_size_u64 = read_u32_le(encryption_info_buf, 8).to_uint64()
if header_size_u64 < 32UL ||
header_size_u64 > (encryption_info_buf.length() - 12).to_uint64() {
raise InvalidEncryptionInfo(msg="invalid standard encryption header size")
}
let header_size = header_size_u64.to_int()
let header_block = encryption_info_buf[12:12 + header_size]
let alg_id = @crypto.u32_from_le(header_block, 8)
let key_bits_u64 = @crypto.u32_from_le(header_block, 16).to_uint64()
if key_bits_u64 != 128UL && key_bits_u64 != 192UL && key_bits_u64 != 256UL {
raise InvalidEncryptionInfo(msg="invalid AES key size")
}
let key_bits = key_bits_u64.to_int()
let algorithm = match alg_id {
0x0000_660e | 0x0000_660f | 0x0000_6610 => "AES"
// Excelize treats any other AlgID as the RC4-style layout: the
// verifier hash is 20 bytes instead of 32, but key derivation and
// package decryption still run SHA-1 + AES-ECB (crypt.go
// standardDecrypt/standardEncryptionVerifier).
_ => "RC4"
}
let verifier_block = encryption_info_buf[12 + header_size:]
// salt size (4) + salt (16) + encrypted verifier (16) +
// verifier hash size (4) + encrypted verifier hash (20 or 32)
let min_verifier_len = if algorithm == "RC4" { 60 } else { 72 }
if verifier_block.length() < min_verifier_len {
raise InvalidEncryptionInfo(msg="invalid verifier block")
}
if read_u32_le(verifier_block, 0) != 16U {
raise InvalidEncryptionInfo(msg="invalid verifier salt size")
}
if iter_count > limits.max_kdf_iterations {
raise ResourceLimitExceeded(
kind="kdf_iterations",
limit=limits.max_kdf_iterations,
actual=iter_count,
)
}
let salt = verifier_block[4:4 + 16]
let secret_key = standard_convert_password_to_key(
key_bits,
salt,
password,
cancelled~,
)
let password_verified = standard_password_verified(
secret_key, verifier_block, algorithm,
)
if encrypted_package_buf.length() <= 8 ||
(encrypted_package_buf.length() - 8) % 16 != 0 {
raise InvalidEncryptedPackage(msg="invalid encrypted package length")
}
let decrypted = aes_ecb_decrypt_cancellable(
secret_key,
encrypted_package_buf[8:],
cancelled,
)
let raw_size_u64 = @crypto.u64_from_le(encrypted_package_buf, 0)
if raw_size_u64 > decrypted.length().to_uint64() {
if password_verified {
raise InvalidEncryptedPackage(msg="decrypted package size is invalid")
}
return { bytes: Default::default(), password_verified: false }
}
let raw_size = raw_size_u64.to_int()
{ bytes: decrypted[:raw_size].to_owned(), password_verified }
}
///|
fn is_encrypted_package(bytes : BytesView) -> Bool {
if bytes.length() < 8 {
return false
}
for i in 0..<=7 {
if bytes[i] != ole_identifier[i] {
return false
}
}
true
}
///|
fn decrypt_encrypted_package_result(
bytes : BytesView,
password : String,
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
) -> DecryptedPackage raise XlsxError {
check_read_cancelled(cancelled)
check_source_package_size(bytes, limits)
if password.length() > max_password_length {
raise InvalidPasswordLength(len=password.length())
}
let reader = cfb_read(bytes, cancelled~) catch {
ReadCancelled => raise ReadCancelled
_ => raise InvalidEncryptedPackage(msg="invalid cfb package")
}
let encryption_info = reader.read_stream("EncryptionInfo", cancelled~) catch {
ReadCancelled => raise ReadCancelled
_ => raise InvalidEncryptedPackage(msg="encryption info missing")
}
let encrypted_package = reader.read_stream("EncryptedPackage", cancelled~) catch {
ReadCancelled => raise ReadCancelled
_ => raise InvalidEncryptedPackage(msg="encrypted package missing")
}
if encryption_info.length() < 8 {
raise InvalidEncryptionInfo(msg="encryption info too short")
}
if encrypted_package.length() <= 8 {
raise InvalidEncryptedPackage(msg="encrypted streams empty")
}
let decrypted = match encryption_mechanism(encryption_info) {
Agile =>
agile_decrypt(
encryption_info[8:],
encrypted_package,
password,
limits,
cancelled~,
)
Standard =>
standard_decrypt(
encryption_info,
encrypted_package,
password,
limits~,
cancelled~,
)
}
check_read_cancelled(cancelled)
check_source_package_size(decrypted.bytes, limits)
decrypted
}
///|
fn decrypted_package_archive(
decrypted : DecryptedPackage,
limits : ReadLimits,
cancelled? : () -> Bool = () => false,
) -> @zip.Archive raise XlsxError {
read_limited_archive(decrypted.bytes, limits, cancelled~) catch {
ResourceLimitExceeded(kind~, limit~, actual~) =>
raise ResourceLimitExceeded(kind~, limit~, actual~)
InvalidPackage(msg~) =>
if decrypted.password_verified {
raise InvalidPackage(msg~)
} else {
raise InvalidPassword
}
error => raise error
}
}
///|
fn decrypt_encrypted_archive(
bytes : BytesView,
password : String,
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
) -> @zip.Archive raise XlsxError {
decrypted_package_archive(
decrypt_encrypted_package_result(bytes, password, limits~, cancelled~),
limits,
cancelled~,
)
}
///|
fn decrypt_encrypted_package(
bytes : BytesView,
password : String,
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
) -> Bytes raise XlsxError {
let decrypted = decrypt_encrypted_package_result(
bytes,
password,
limits~,
cancelled~,
)
if !decrypted.password_verified {
// Parse at most once to distinguish a tampered/legacy verifier from a
// genuinely wrong password. Workbook reads reuse this archive directly.
ignore(decrypted_package_archive(decrypted, limits, cancelled~))
}
decrypted.bytes
}
///|
fn standard_key_encryption(password : String) -> (Bytes, Bytes) raise XlsxError {
let password_len = password.length()
if password_len == 0 || password_len > max_password_length {
raise InvalidPasswordLength(len=password_len)
}
let salt_value = random_bytes(16)
let verifier_hash_input = random_bytes(16)
let key_bits = 128
let encrypted_key_value = standard_convert_password_to_key(
key_bits, salt_value, password,
)
let verifier_hash_input_key = hash_parts("sha1", [verifier_hash_input])
let encrypted_verifier_hash_input = aes_ecb_encrypt(
encrypted_key_value, verifier_hash_input,
)
let encrypted_verifier_hash_value = aes_ecb_encrypt(
encrypted_key_value,
pad_zero(verifier_hash_input_key, 16),
)
let writer = ByteWriter::new()
writer.write_u16_le(0x0003)
writer.write_u16_le(0x0002)
writer.write_u32_le(0x24)
writer.write_u32_le(0xa4)
writer.write_u32_le(0x24)
writer.write_u32_le(0x00)
writer.write_u32_le(0x660e)
writer.write_u32_le(0x8004)
writer.write_u32_le(0x80)
writer.write_u32_le(0x18)
writer.write_u64_le(0x00)
writer.write_utf16le(
"Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)",
)
writer.write_u16_le(0x00)
writer.write_u32_le(0x10)
writer.write_bytes(salt_value)
writer.write_bytes(encrypted_verifier_hash_input)
writer.write_u32_le(0x14)
writer.write_bytes(encrypted_verifier_hash_value)
(writer.to_bytes(), encrypted_key_value)
}
///|
fn encrypt_package(raw : BytesView, password : String) -> Bytes raise XlsxError {
let (encryption_info, key) = standard_key_encryption(password)
let raw_len = raw.length().to_uint64()
let size_bytes = u64_le_bytes(raw_len)
let encrypted = aes_ecb_encrypt(key, pad_zero(raw, 16))
let encrypted_package : Array[Byte] = size_bytes.to_array()
encrypted_package.append(encrypted.to_array())
let builder = cfb_builder_new()
builder.put("EncryptionInfo", encryption_info)
builder.put("EncryptedPackage", Bytes::from_array(encrypted_package))
builder.write()
}
///|
pub fn encrypt(
raw : BytesView,
options? : Options = Options::new(),
) -> Bytes raise XlsxError {
encrypt_package(raw, options.password)
}
///|
pub fn decrypt(
raw : BytesView,
options? : Options = Options::new(),
limits? : ReadLimits = ReadLimits::new(),
cancelled? : () -> Bool = () => false,
) -> Bytes raise XlsxError {
decrypt_encrypted_package(raw, options.password, limits~, cancelled~)
}
///|
fn bytes_to_hex(bytes : BytesView) -> String {
let digits = "0123456789abcdef"
let sb = StringBuilder::new()
for b in bytes {
let v = b.to_int()
let hi = (v >> 4) & 0xf
let lo = v & 0xf
sb.write_char(digits.get_char(hi).unwrap_or('?'))
sb.write_char(digits.get_char(lo).unwrap_or('?'))
}
sb.to_string()
}
///|
fn hex_value(ch : Char) -> Int raise XlsxError {
if ch >= '0' && ch <= '9' {
ch.to_int() - '0'.to_int()
} else if ch >= 'a' && ch <= 'f' {
ch.to_int() - 'a'.to_int() + 10
} else if ch >= 'A' && ch <= 'F' {
ch.to_int() - 'A'.to_int() + 10
} else {
raise InvalidBase64(msg="invalid hex")
}
}
///|
fn hex_to_bytes(text : StringView) -> Bytes raise XlsxError {
let out : Array[Byte] = Array::new(capacity=text.length() / 2)
let mut pending_hi : Int? = None
for ch in text {
let v = hex_value(ch)
match pending_hi {
None => pending_hi = Some(v)
Some(hi) => {
out.push(((hi << 4) | v).to_byte())
pending_hi = None
}
}
}
if pending_hi is Some(_) {
raise InvalidBase64(msg="invalid hex length")
}
Bytes::from_array(out)
}
///|
test "base64 encode/decode" {
let input = @encoding/utf8.encode("hello")
inspect(base64_encode(input), content="aGVsbG8=")
let decoded = base64_decode("aGVsbG8=")
inspect(@encoding/utf8.decode(decoded), content="hello")
let result : Result[Bytes, Error] = Ok(base64_decode("!!")) catch {
e => Err(e)
}
inspect(result is Err(XlsxError::InvalidBase64(_)), content="true")
}
///|
test "hash vectors" {
let data = @encoding/utf8.encode("abc")
inspect(
bytes_to_hex(@crypto.hash_concat("md4", [data])),
content="a448017aaf21d8525fc10ae87aa6729d",
)
inspect(
bytes_to_hex(@crypto.hash_concat("md5", [data])),
content="900150983cd24fb0d6963f7d28e17f72",
)
inspect(
bytes_to_hex(@crypto.hash_concat("sha1", [data])),
content="a9993e364706816aba3e25717850c26c9cd0d89d",
)
inspect(
bytes_to_hex(@crypto.hash_concat("sha1", [b""])),
content="da39a3ee5e6b4b0d3255bfef95601890afd80709",
)
inspect(
bytes_to_hex(
@crypto.hash_concat("sha1", [
@encoding/utf8.encode(
"abcdbcdecdefdefgefghfghighijhijkijkljklmnlmnomnopnopq",
),
]),
),
content="967b4ca81ed24d3fad00dcd88994c4e380d1090a",
)
inspect(
bytes_to_hex(
@crypto.hash_concat("sha1", [
@encoding/utf8.encode(String::repeat("a", 56)),
]),
),
content="c2db330f6083854c99d4b5bfb6e8f29f201be699",
)
inspect(
bytes_to_hex(@crypto.hash_concat("sha256", [data])),
content="ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
)
let sha384_expected = String::add(
"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed", "8086072ba1e7cc2358baeca134c825a7",
)
inspect(
bytes_to_hex(@crypto.hash_concat("sha384", [data])),
content=sha384_expected,
)
let sha512_expected = String::add(
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a", "2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
)
inspect(
bytes_to_hex(@crypto.hash_concat("sha512", [data])),
content=sha512_expected,
)
inspect(
bytes_to_hex(@crypto.hash_concat("ripemd-160", [data])),
content="8eb208f7e05d987a9b044a8e98c6b087f15a0bfc",
)
}
///|
test "aes block vectors" {
let key = hex_to_bytes("000102030405060708090a0b0c0d0e0f")
let plain = hex_to_bytes("00112233445566778899aabbccddeeff")
let expected = "69c4e0d86a7b0430d8cdb78070b4c55a"
let enc = aes_ecb_encrypt(key, plain)
inspect(bytes_to_hex(enc), content=expected)
let dec = aes_ecb_decrypt(key, enc)
inspect(bytes_to_hex(dec), content="00112233445566778899aabbccddeeff")
}
///|
test "aes cbc decrypt vector" {
let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c")
let iv = hex_to_bytes("000102030405060708090a0b0c0d0e0f")
let enc = hex_to_bytes(
String::add(
"7649abac8119b246cee98e9b12e9197d", "5086cb9b507219ee95db113a917678b2",
),
)
let dec = aes_cbc_decrypt(key, iv, enc)
let expected = String::add(
"6bc1bee22e409f96e93d7e117393172a", "ae2d8a571e03ac9c9eb76fac45af8e51",
)
inspect(bytes_to_hex(dec), content=expected)
}
///|
test "cfb roundtrip" {
let builder = cfb_builder_new()
builder.put("EncryptionInfo", @encoding/utf8.encode("info"))
builder.put("EncryptedPackage", @encoding/utf8.encode("package"))
let bytes = builder.write()
let reader = cfb_read(bytes)
let info = reader.read_stream("EncryptionInfo")
let encrypted = reader.read_stream("EncryptedPackage")
inspect(@encoding/utf8.decode(info), content="info")
inspect(@encoding/utf8.decode(encrypted), content="package")
}
///|
test "random_bytes draws differ across calls and runs are seeded" {
let a = random_bytes(16)
let b = random_bytes(16)
inspect(a.length(), content="16")
inspect(b.length(), content="16")
// consecutive draws from the stateful generator must differ
inspect(a == b, content="false")
// and must not reproduce ChaCha8's fixed default-seed stream
let default_rng = @random.Rand::chacha8()
let default_stream : Array[Byte] = Array::make(16, (0).to_byte())
for i in 0..<16 {
default_stream[i] = default_rng.int(limit=256).to_byte()
}
inspect(a == Bytes::from_array(default_stream), content="false")
}
///|
test "set_random_source overrides random_bytes" {
set_random_source(count => {
let out : Array[Byte] = Array::make(count, (0x5a).to_byte())
Bytes::from_array(out)
})
let fixed = random_bytes(4)
// restore the built-in generator before other tests observe the override
random_bytes_override.val = None
debug_inspect(
fixed,
content=(
#|
),
)
inspect(random_bytes(4) == fixed, content="false")
}
///|
fn decode_base64_multiline(text : StringView) -> Bytes raise XlsxError {
let compact = text.to_owned().replace_all(old="\n", new="")
base64_decode(compact)
}
///|
let encrypt_sha1_b64 =
#|0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAADAAAAAQAAAAAAAAAAEAAA
#|AgAAAAEAAAD+////AAAAAAAAAAAHAAAACAAAAP//////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|///////////////////////////////////////////9////BAAAAP7///8GAAAABQAAAP7///8ZAAAA
#|/f////3///8KAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAEQAAABIAAAATAAAAFAAAABUAAAAWAAAA
#|FwAAABgAAAD+////GgAAAP7/////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|/////1IAbwBvAHQAIABFAG4AdAByAHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAWAAUA//////////8KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAODbfLh1f9YB
#|AwAAAIAGAAAAAAAARQBuAGMAcgB5AHAAdABlAGQAUABhAGMAawBhAGcAZQAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAACIAAgD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAJAAAAmB4AAAAAAAAGAEQAYQB0AGEAUwBwAGEAYwBlAHMAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAABAP//////////BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|8CB4uHV/1gHganq4dX/WAQAAAAAAAAAAAAAAAFYAZQByAHMAaQBvAG4AAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAIB////////////////AAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEwAAAAAAAAAAQAAAP7///8DAAAA/v////7///8GAAAA
#|BwAAAAgAAAD+////CgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAA
#|FgAAABcAAAAYAAAAGQAAAP7/////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|//////////88AAAATQBpAGMAcgBvAHMAbwBmAHQALgBDAG8AbgB0AGEAaQBuAGUAcgAuAEQAYQB0AGEA
#|UwBwAGEAYwBlAHMAAQAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAgAAAABAAAAaAAAAAEAAAAAAAAAIAAAAEUAbgBjAHIAeQBwAHQAZQBkAFAA
#|YQBjAGsAYQBnAGUAMgAAAFMAdAByAG8AbgBnAEUAbgBjAHIAeQBwAHQAaQBvAG4ARABhAHQAYQBTAHAA
#|YQBjAGUAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAEAAAAyAAAAUwB0AHIAbwBuAGcARQBuAGMAcgB5AHAA
#|dABpAG8AbgBUAHIAYQBuAHMAZgBvAHIAbQAAAFgAAAABAAAATAAAAHsARgBGADkAQQAzAEYAMAAzAC0A
#|NQA2AEUARgAtADQANgAxADMALQBCAEQARAA1AC0ANQBBADQAMQBDADEARAAwADcAMgA0ADYAfQBOAAAA
#|TQBpAGMAcgBvAHMAbwBmAHQALgBDAG8AbgB0AGEAaQBuAGUAcgAuAEUAbgBjAHIAeQBwAHQAaQBvAG4A
#|VAByAGEAbgBzAGYAbwByAG0AAAABAAAAAQAAAAEAAAAAAAAAAAAAAEQAYQB0AGEAUwBwAGEAYwBlAE0A
#|YQBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAAIBAwAAAAUAAAD/////
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAHAAAAAAAAAARABhAHQAYQBTAHAA
#|YQBjAGUASQBuAGYAbwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAQH/////
#|BwAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBqerh1f9YB4Gp6uHV/1gEAAAAAAAAAAAAAAABTAHQA
#|cgBvAG4AZwBFAG4AYwByAHkAcAB0AGkAbwBuAEQAYQB0AGEAUwBwAGEAYwBlAAAAAAAAAAAAAAAAAAAA
#|NAACAf///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABAAAAA
#|AAAAAFQAcgBhAG4AcwBmAG8AcgBtAEkAbgBmAG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAcAAEA//////////8IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADganq4dX/WAeBqerh1f9YB
#|AAAAAAAAAAAAAAAAUwB0AHIAbwBuAGcARQBuAGMAcgB5AHAAdABpAG8AbgBUAHIAYQBuAHMAZgBvAHIA
#|bQAAAAAAAAAAAAAAAAAAADQAAQH//////////wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBqerh1f9YB
#|4Gp6uHV/1gEAAAAAAAAAAAAAAAAGAFAAcgBpAG0AYQByAHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgACAf///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAUAAADIAAAAAAAAAEUAbgBjAHIAeQBwAHQAaQBvAG4ASQBuAGYAbwAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAIBAgAAAAEAAAD/////AAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAAACEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////8AAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAEAAAAA8P3ht
#|bCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJVVEYtOCIgc3RhbmRhbG9uZT0ieWVzIj8+DQo8ZW5jcnlw
#|dGlvbiB4bWxucz0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNi9lbmNyeXB0
#|aW9uIiB4bWxuczpwPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL29mZmljZS8yMDA2L2tleUVu
#|Y3J5cHRvci9wYXNzd29yZCI+PGtleURhdGEgc2FsdFNpemU9IjE2IiBibG9ja1NpemU9IjE2IiBrZXlC
#|aXRzPSIxMjgiIGhhc2hTaXplPSIyMCIgY2lwaGVyQWxnb3JpdGhtPSJBRVMiIGNpcGhlckNoYWluaW5n
#|PSJDaGFpbmluZ01vZGVDQkMiIGhhc2hBbGdvcml0aG09IlNIQTEiIHNhbHRWYWx1ZT0iazJnUjdQa2pz
#|WXZKNGFreGJTa1pSdz09Ii8+PGRhdGFJbnRlZ3JpdHkgZW5jcnlwdGVkSG1hY0tleT0iaWIxZzNIV01T
#|dWVkU1cxbEJkeml3c1NST///////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|//////////////////////////+DHgAAAAAAABUGNYtOhqc4yUj9wgYGQV42fwvbcRbwK4nLn31yN8Yg
#|bJEmSxo4acfo+RyD7TYTd+q2jjGAujGOHhxO6+8MXyeT77kbpNQajlHGgCnLpJ9t3F/vrQWlIcm62ONk
#|ajmo8jSl5LZnSLB2Z8SSdlzwcPpZI2+l9P2wyBYLQPuqHwsb+KPhBHZMu6xYoYp6hvTelNV/nKrZ3ZJH
#|onvzr3xY5dAi53ZxP6yyQm1xFmTcSOCCKNhTCXxrC+RafEt4fWFwp60R5OmVujOiYSAs9R/FFSunXVfr
#|2yBy4DSJVqnopDYC1nV7JXY2yR1ImtgQA/pEL9x1P9g71NCDJ3CCqtWJvxjNnhNy6vVv8fYZStfS3y5w
#|KsbtEfulmrJJCrf1eR4HGrBOzbYmep0WJMGMIeAGt68u5reuzDNte2aX8Lgb6Gf7cSM6gI0yDNstrcEQ
#|mzdLgXiCv6ROMyY1YDZl9a7PjKc0W+099YXC/iu8zff6WQunfpxdWVryr7aIRlNY3hYTbyiOwp8shv9B
#|L9lIXUw2+XgYdYRVXuuC6ITadtiXC1eHYrDClhxi5vyFlx/BdYGA6QoeWoY4Z/6NhsiES+ybuer1Zuia
#|+hyC5o0Ap+rrxTQFMLwnBz1nIujXHTMnlqAPWVGOAcjxr6n/7TMyaGI7rXm975Qkd9CZNjLB799zXsG0
#|V85nf7M/MEexFUTpT6c7X8S2HARcmC5AfdsQeu3w9LsuMXm8kXw0E2EA9NCCyPD0TLrQagxhwfoUsh2c
#|/Gx+t6/P5M/+X02vviXpPIckZT03kR6ejTGQZu+uzAErqvF4Jdp62LGteZ7hHvqEjyYStrJSH4gtlt/k
#|NErxszSWhjzlByftoIUa4Jv6mB7ba/0k+im5GbXTo5EXd/gNyLvmCrchR939AbgpYYSRQWQt1rZzzpYH
#|/k0P1+kibb4BMmbyZqyym4QVCOd25pLpgrtGrtMRHj0NoqIx8gDjblU/m/tfW3i+RbiIqxToObE2tjbJ
#|GmgqOHjba5jo8seGomvHZqGRegWqE1MZTUNnVsVsjb1+chh33ny+aCc3LzdYyqyeJjo4teXCjeGOISo1
#|lPu53UAwPe+t2UeKHeQ6UKCyQuRVzk73k8KSSjaeg7PdAyFwBXz/JXRXgAWW/OBeqJuzmJwQigP95Nnf
#|L0HT9CKy5cD1V35GPFTK1exfAbqrzS8ENDFQAUOmKepLWHZgAkauPp9SYX1eaztLbtOWk55TOtkt3Fq/
#|ILkE4+8MKyoc3fA4hs3RV/wmhRF/UHfKzAd82hU7+/Tnz8Gd92upTws2Czv5mrc3V1XJB1ZAiNpoS+5U
#|8BgLgeDbA0LJTYD/YfO+Rv+J2E28FZNeItrcFHA0nTjqXave2HJFiUP1iba8rYweiotoEPeqzkeww+io
#|dMfksyDLLCCBXu3i7ECLj5yihX1ZZi4J42ZVTvXnvjyqOujcjsdpJGgw0tkCY9PJOltb4Yqui4l4ybyK
#|Wu1YqPoRGLTs0DpSlbW82MyQfjcqyikIl6+2bmC9SwADzA8DOgF17FWhYzCm7mqim7MwG4geWmZ2p32T
#|7QhNYGCZLsLQ8LrcoN1+HYEJsfZkkHy5e4EfXetMpJz7ItlPwnBJr3ieUmmBZmAOKg1pRUlX9mi2HO9t
#|+TUG8arqaZEWIMMiMIVL/kto1nIvOW1XBW6jhtuH2kJpVYV467xlgfzVH+ewuWVespo6hu1IvmlvCyCF
#|Luk4xz6k13bNDp1hDEOs94evCq0ehNv+S7rQG+Oq1dW3QtjtRhVIFjqxrOkhROsFnOa7RmbCyZ+5ZS67
#|NH2N6oV8gyb1vRNfzR9IkEH/pGpbpXewYnymKGDsQoc+o8i3RU8/+1bruiFZDbHsyfs2raT3234wS64L
#|vAWTmu4LOGGtyjUd4rD+a+DpQtj5X8LS7OOcIoVmatR/WPQcPM2Cs+q39yq3u/YiRlqtIXL+P8mpxpwm
#|rRMl5MYoIcbv7jbF8a1Ab0rbkymxXgfT2qCO5NE9a76RehgWiy1RtHokFsnhM7nVNCtjxgtQq52h7H5h
#|j4QcCTlpQ2KafEzwrtk7KjGoxpyBxsmHIuA6ZHHw/ZeiSCWlR4QkZhd1L8dUbiH08KNkRUc8msguqcld
#|GVBqWlfv0M6CdF0MmU1RGRbN17c6m5GZPJM3ii6pkfc8WRHxJhkdORmuTmeqqPXdXgAdgFRZTBsivPHT
#|cvIOv39Df9/V42jOS3WuQbOTzAfMA17U4rhq616iz9qpaskbbPYUyUdJmLMO3c0QREltvcof1YNZnOOl
#|XeBc5J5RRsNamALNpqQKllwirhwH/VJYxB6lthQY7t0nr/3LeDXLxob0HXjXz8D58Wmxm3Xb0tSI3AiG
#|ZDOLBuV8iE1LGS+nVhuqcdS9Rl4ssRfcgIAgytlwEbRxmiUuQmPLHr3zIFppGJAuO711B03FEBZQq6iD
#|/I7OguswbSm72/rFccSZ+zb5RUH7LSHn+fuYerjuI3vP1tO8Xd6Mxdja8ceatmK+mHYnx6OCNk2zizKN
#|hD6YuGBh834jEIUQpSRAzTmK7MdYZZW8Mvn9viEkrt9owO5C9rHwewlNKKVDzVWM6PHV2r6z6sEiAy/l
#|yJ1PQ7mu2Kz9R1Vn2YYECHqSm0YfLSkERZiv7tBM2dhn6WKYWRSQqhCPz/upxB6i127CeK4mHe3VkfZN
#|5SgjDfa1ZCLVn43bL+JLYe3HZZnp2NFKizCIxTNYFc+oa3DYk+q7N8v44FuT1MSaWpERGRtibUr2jZ3K
#|IuICXlQOVNxzvTKDmgJfyU/OUr8Jov+n5Bp5kG44G5rFYvSJ2zfWIBdZ89GIHcc0xUTB117kFGC2RuEf
#|MLkT+qAMti+0Z2tPAvQE3fC0uOj+kQvguf+GRrjd2m8GuWXd5/9ZRf0v0W3gktsgjYnGSCWwJ0CRsAmp
#|i93K2pajGVWZkecMnaZTLhxGKfo5BrIo8oEZSMJ5IOXSCQdcErPifrwIVUvHF3xIiKBEXG8o4zl1YeUr
#|ICJCEQaXR6ocNNi4Eti4VyQbF5yssK/7ksDa0+bk2z9ncTYzjy09b4NOxxOuKo+7/l6s8h2B296CFUMT
#|W8Nznx5tokZKm5FpuzR//weZU+F+K0m2wP+zOjrnKPczS9e+0Epvpz7XPz1pNaFlsMgwCtgosvUzs+cc
#|UoodGy6/nXQxGigbQbwrby/2CFQt91oQqq4jz+ijcvCWid+hwvADqWzM2PgE7iFefdbB5Uj8nkmTtf6J
#|O4gZAbeDLsOsgl3SbkiOM1+Y73YdLs8Am3K9lYC0B1mrUIrmfKwPhUs+OYxzDFdYSFwA3e4bH2EjgeUI
#|948XSelibyfiT97kP/0vsrXgf1PPINeZm5q5u4pnI95olDYIBThix7kkLpb9SO2AtbVgVtTVS6spC8Js
#|5cYN9lei28O6aDQx/W5pgJXQQhKom2sQUtENWQjY6J+F3XS+RjUvkRnR4gwasSr53SlR1buco3sqCrpx
#|AZnJb1vMS/7JkmYCTz2Hibsr4LsVH255eEGkddHwV5PnKtLZ/Sic//pQS3fzLgzcv5fcCQuNFuXpu7cn
#|oo0EmLkZa1GWH4Sotpc6zPxDHsdDSV/GldCq/dayS3VAsT+zZDEdxJIvUareocqzi9sTiW1QpPTaa3sW
#|+43L9F34pzk5MIxlOJxaA0oY7HI2yRKSXnnbUbzPoVyfzZ6I8lS/M4iAyIvd74rhvYU68Jouqz/tBuqe
#|PhJys2ylte7BFk4MIl44Blh9zCtBxgz2xJOWApEOoGVF0nCJjRA5SC50lHMX9lil8sHXXCXVVqO4lA2o
#|1LCWY/ONguOJvnCrMiK/NQBHgDv5RXqmf7lOnoZ+QZB/rvd3EHq5CipTy9tX76HKIzWe49fx/F0LXQAT
#|scWYCEItUEVo/tTjOgQDfgQSXmww7b570i6zj91jhmOuuNMpQp6grye20yqE4PTpSF9R72+Es2nTpBKE
#|pNoo3bkFgwAGnEToN6YlbOtgIDNlhyDyqsc/TwHgaMUUkFQiFJkABpMpUDczhFx6NZhY6Txk3GLnHD3C
#|Rf6kYMrpMSqoWb1rbJ/k6j+tUtXqDVCgo/GjPp2GbX2UTC93bT78UF7Vn1Yb/vjldk/qwVhkQmT8dkdi
#|QofCPd/MqBxHSgaafTkwikqeJYiDi91mCqMdylzyEbzxW881//xcmJ37Q2FxkQ2BcnG3Y/H6qjUygLJU
#|WC2UH7NfhDuvOaZCPBx5hQSmfHAgX+fiRXbEJNYuvqTi4qX6r5SXRpkOBB/YTBYoyizXFZxowxlOO/FY
#|+afsvPw3ppxNgUxX8JvPTi0g5+M11jQdweU/lG5g5NC5iDPDmlmNDXyHTUHyTyF0+ljXTH8A/VFIbT07
#|CqzUEsvwLm98QqzbC2NPHQ1WBUkJUe93m0/0JMaX/x2xNr6d5rbTx/NrmhR8YNMjyuHa9sdeyLfk9vzJ
#|/RChbf3QGlNUdBR+VJYWwIFfdqXf3jHkw/rT+FhLLbSIo8Y7O4AlSPz5jSiv6xXRFL3i3XBukNS6RWFE
#|9X7pHJ9IqI+yZBkd0QzC4atf/EoY5O76X+aJopG++/hi3C1eHSVIfzEfcUeNwKpBoam2ptVhnlXNBw8a
#|v42nkgPxb7O+ka8PgR9R21Vb9XIlFjf1IsqMKua+ZfCBjXLmIkRD21SUujQoXbuAggVGbiXYcg7ewBB8
#|EbrmDjgrRZd/rJqmr8JF3CKTeNJtqbZ5QDn5AmFwC9IAMZmZtHpdAZbrXzsEI+03NEoIXEqG7C1L+rNC
#|cd8EgHE0Z1WCzVadzjx8RwyN7UUQ9TtbSE61N1qq66sKXYCYFVBnOicbZDblXE8UUewfHuNBrY4iyKpF
#|6iv1Qh6A/boY9iWEYwEF5cGwbON2Ou64OfY8Y1m4zy9K5cjYzko697hzpwLdMaBLSLQFASMoWjxbUuFU
#|QnJ5+D5IVSDmqB6dCe+cVHfHn1WDf0wrWtDS6hYSskhe+mn7VJfQpNiGzvqn5nX02KRwh/V+JwHq+pft
#|bCg2pE9Epz2Ok7rX6Ng5aEe28RDqjhKqlPpudq9wT+9tsYoFAjEAkHmWtzRmZGwigDFdWhWsulAfRXTD
#|yktq+lBKA1d8QvuKyZJKdVFPZpm5FAEjJCns7xjKh08DSzQAGcmvLrY70IyCUKI26RHxJkUExf2KIYtU
#|cRq/ra9bDTvLynCeFZSFThI35Kx2lo2BF8tuiDjOiScR8QKU6+Y+oTYYkzgpGgXbPokLXUgdcH+fpqjC
#|eL5zao+qRM2kmyu6cCtfg6QBYiDEZXBTcBF6yvgNc++8QUpuJYYbOccIxw28Sl8cr2rp+uv1uq36w4QH
#|GhMIZsfN6o5m3MDNS4bvvJmP8rhLiqQ6NSCWCmFtY6ai34dpcveOPw2YHmgglHw8cEx9N48Jsdx7Jb8w
#|E6DJsU2sYnn53nHFM3LugCQRBVEOcWB2EWESMLHEFkMP/T6ep3iuz9LjqnmLEqbyP/deTVXYTMWUSCW5
#|Clm3lMBw1bGR/iAbpzMBWdXu9L8/WS1dfbqHy9dD60BHypVwXn+aTGWv0/MXerFKobAeTx3NASN15fjr
#|0dQrl6jl2Oe7Pt9KxP+uoAyXt5+RlWa2049iIB8WRxyca9ZqHsdKP4PTh+RksO3RXdQiar8kFEbxUfZ0
#|/lzrj8dniRFzgQfOtU6wCDoFgULc+ac51MXkmzwpRDJQzH7FzIYoXyHeM8bIyp+V9n9+CsingDM5CIOd
#|2d8Cz213A+4ak5YFpnWZGn/a8zC214GQGftms0FRmYiEOn02ev4uzd5l+lWVM7rZ/l5W9aEW0/1jBoBC
#|yDXbcZ9LZO+bQy/315Zg1RadyFvsMgnZxJHXzdsAXgsaHanx5DflsN6gXUxzeha1B4jbN5NOHCONIbOw
#|MgmmjuVh7t8nghP/9YKsh/nRxsrDae+DmtOpJbqGuW+iu3AgxNaFL/tvJR2q3g9mwunQgrrzIV7iw1pZ
#|gPHNEVsRn7quXPEa7FWoVs8Djv2lO3i/wlolKLCzdh1Lyy9fFHAPLFU4qpjuUR/NAVpnlqJlLqtiQIYc
#|JG9ycFzIAa/+8wbuO2tWrWmGN+6XEI43sLjjNDsmQUgny5McYgxQ56vY/Cig3TMeiwQ5Sw80zgi3UXXa
#|JaIfuvIM7PGqGmAFwtf6rtJsiCvVJSzr1YRlq2cmAdajHU5296CFmYat8nk6MQa1i6bUAlWONSuR7nUC
#|+yuAcoNBpANBlUT6oomDTf2RYM36gcjOYZpvaJuXo2T+LAlC0cWv3AE+uREWeJtw8G8sXzWyJlBRXCP3
#|rhL2MGUfWtggAXHn4M+j9DQbvqEA+qG4MrViFturYY3+9cRWDKwiABWYvsdHVtCpttSF2TM9s+WPlDP5
#|708irzhEomciYSCMhkibLOcoSDyl6m2vJS+q1SUZLEDHHHmU3kyAOPf6GRiEfnFm+0/c/jd6iFTTVBea
#|zwTxnB6kclblCsdTu8orFh9c7oZqZH/11G71of93ZJzxN+0kJoH+g1ArxS4gBHyEORdK+3XC4aB5xPyq
#|hY4BJj8qYWjMYWQvllD/QZEofMkKo/EbRoUK7Zx9uUU3YetohYRBRtAwEnLb27AMUyijaTydOMDKM1Oi
#|39K55K7XTNQNQQw2cZLNx4lppHSPvkKsbZwH3PjjlobXol45FxsczYwwdqd+68NuHQJr24vIcXgfIJ3X
#|F9jeEZYAOJ1ZIEroalv/6XqADcR8+QeW9PJdjZGN1LIe0KKaWoX6T73TyF9rKQjf0sSm3W2JgMzUwydS
#|wEcVEmwza/8QkqsaaFP25EBsYGHOYxupKTDdqpDem1dfbErzbmOhHaFjG9Nd+Ojz4urVxjUoDM14SoCX
#|k+9H75oBsaFvBb3EaciYWQAC3e6nagjXofsnVkqOCO1sxU+OhWHi5cwyS3TLWL6DRgKgebzVGlgHII1m
#|WnYkERTE8HlzYJdbi3yCORDchWvqz0KFK21q/Po588MMQJimFbcpHTWSSl6c/gAYTRMKyURR5gdU8ta8
#|u8PaLNyL6NpTqBqXjyfQXZNkAqHth6Ksp8kJyqqtfD1qnFroWy5D6DdgDZVw/8lJCDsS5VFwcAuJGNhH
#|W32WiGwks14PmeVY1e8HpzFuKCRgdjvU1SFXR7y/JSbnKVQ21Qs+1dK13ZehIc6Rpb7CmX7IWiIMUss8
#|h5ALuwmqb+JuF53rAj0DSmqBxw5EjqacCrANY6I1dRlBi8hOleDOKq88uR1U/va5Qfr0c2nnjk/AUe9N
#|du4GjfDc8rk2U5A6MuHeswHlBPCVP2lIDcNa+fEr6gkbkzF4lzEZfTH9/AQ31t2YaZePb7cKEXdHmu+H
#|UIS2bHBhDMVLvPDzg98QrRIrIAsn9+2XjZbCv2y+OzZuDz4QqjXpcxMgPQ2ee9OAK6VILqZm6ePBTrlr
#|5V0wCabk7QvRr0DtdBkpzvO8+05FKtHKJjoWLo4NqfF3PrPgrI9OrjdN+miOIvhAAuWs51APbtp4m+QP
#|/5RzZRVLXPp2VVTlsA4Lt3NXp6MWLzpQ8BhT4S1GobmU2/Vi07sxGKhyL2ipGI1j4cmLoFd1hf/Y6kSQ
#|Jb4PhWWkyTm8g7fLj+9x+92rQbnxZsdXRKNGGFdZtM1OXkcnZy2wnAJmwPPJFZrzYgHdxaMMV5sTi93l
#|QWLQ7v4kHrRVh/iRb5DPuNdXDWAQjAqxNpsBtPpbcbcxHHVPDMGIDwIwyixVKh9JAh1eiqjB8KPL9lCk
#|VwA4A9tXBkq6/NopNViuqv/scDxD5IHDtljdJZZGdoZUFzydGdO4nfOUi8Csn1VYiqkyOSjJlq3iKjDI
#|ReAGqSDXrNb8z6ktgnzUm08cB7epnyOWSM90sS7MCWVephxUPFFFrYc0zGK3yHKfUxnQPPW+887XrDA0
#|JDQOu7mVbGJyjVZFI6Tx8zbL+9CmujfF0DBZpN0IEa61R8Suj1M68x6yQdMfvLZjrz1dSXglH6YVJFy3
#|Z6pWEi5+73Bs9oMMt0U9cBfae1lXQG4d6goPRBLg4GbHrL2arZZTs9uLnCyl3ZNeXioRQu3pDgIbZpl7
#|hkjDVgdXfEs2sZTy1i2leap6vYp4j74fUICd0VSVvP/sq0QrkjSqUtSqDoxdB+IfRYbz1E5RLMVbef0v
#|rPdx9KBxI+ch0CUd3x8ik2w2IzO0JERGzbW/6IcwJ+GiBocEgKW5AFZhZHMpa7ag2gLB0GzmIb+a5Hrm
#|zMHoNUUOjsW7vPOSWoQf8ts292YsRUKDUBFDlefFSDQiyV2kzPz2+U5CZBK60YnObwn59SlpvG62PfxU
#|Goj9h9f7gV711c4sRuQPkB2oGSISiW+W3D2iTL4TETz5rduwb1+cbGF8n9g1fkKyLdrxNsBTG9U3IGhR
#|+KwmMROhqwrZl5HWiKe3HDoQ8NzqP/tYZ6GKhE1Rt19SwP9wZcNQYDaps7APHn4OcSLoxye6vxgr8LvH
#|mPL32/4ROR04OAU79VsMLXU8E9JJ8AklW/fe5etcKgy99mZa+tYUIWEMTuVwDUO7ktouahUIGLuK27/p
#|elgYXfk281UcXI2575pcN+rDmfH0he2AA2tA35+cA1iSHUvN119NE3k2TvbJYG6f4KGUGOQRnILEtG3y
#|DHaGIxOCPPjHvQpnrpH0lq0WW0yJ02w4CsJcY5TCZfIa4GHpn2j/zh6v7V4fYsk/4MOANuzDh5FV4an6
#|bJhvYdrIvBrzgkfFqaCQth0D1rS/CnycX/GrotQzZbGunQdrU3UWopeUtlRya5EsefNcAPTSTYJmwWYV
#|G0AuAEuvnLmbyAfBHoDXDapbqYvnkFyatb4p0mm6gh1rjEXfVrYEiUxMQGk+lrca8GqeVsDGjyWIxJsq
#|fkhuIcgrPzcIh5aUM7T6b7Ojw5pJC41k5NAUqAkS1bKB2YA+VqJqEANrRH1AaBsAx22kVvRTG62pLy8p
#|/ogVvZ7NzZIeaAG7CYY8ZhjEobu3o9fXP4aVInTfZ1BZo05L2IXTJZOSX93sisIlxiaQwJwefTSPOlEI
#|gTRBsZbDq+5O7VTpcxhz2zSpo0PX2jI5249zzJxZdqDaA+DrYrazG4kOmhrxsa8RodyPnrtXuqbGSeXF
#|XvsmAQezJJV8sYZHiL1KxVskQinDx3t7wCzhaLkwjYIcVm5Nqb85EGWNI9ifdA/ZAizG4dnC7+EAcpuy
#|TaOCFimd5+r6Tz/nyuzDlppnkO6HUbScMiajU67z/U173C78r1CudaOqLHGjrvCUMRdsi+4JpNAqedHs
#|0tGB7O0WfldAzG7Ry+8DKn+phMl+x3va9/6x4nkOl6lmx+6k2a+vTOSI0M3DT7Mahe3g+H/tq0BopXL0
#|QePZ4mWbcmQiaG29k500ONtsEmjQ4+Y8VMYZOHppuwhBcN/7lgNG9i+S2EdDwrxGdw8IEoWJ8BgUbOlj
#|l3iutwmJaSDSmwaWhZMJ6bXug35Cymj1JsxrXNarpqYK6PexTGMWDxMnnvcEEm5qnlQCxDCQEAQK+V4s
#|ifeBoHQLGqvEqKvOlGfe0ZLYzHF6EGJVfsIMkd7pqNv7hDhDQhnVvC6esF0NmKlEvLd0ym/6L/vq+nr7
#|kLfUpA4W1yHUHxx3YwyARxKkU3UJPo7zf987j8MJj/uS6m0hNWA/peiXbTwosZIZaEIMqt//xA50cg0J
#|ZG4twBmF5a+ABCPZxEN+X3uD+oteHofI09Xn9JJ117ejkbVefDl3QAHIWw8lLVHj+Uz3Xcq18b1Kfote
#|alhN1x2Isk6aWDrOJEXcamUt0nMHj3jt/YVL7dI4fFtrlN7LQ6IqVfSGHcOfXtTFRfqczVCCJ2RJRo5g
#|qKM6O59cza7C8yoolp4TqbbtvHyLMWElexVC8QsdBLrmqxEw5qTfTYtf1jsq6cgCfRa+s01KqaOF6rOo
#|2i/ogUrUcKY5R3fAGRIk/20Usqa9ptwbrVVymn7IPCxY8LidPevxwGdXKzDIKh2h1WliCbi/NVjEC4KM
#|AixwK45M0icknKWlpU8+dHdcrQcuzwMAb9HdaaQTF+1G/umPjlPmMxH3OyKXm69fkQiyyCe47SiT0VkH
#|C3IBNz4KjJAeDhv9BqtXvEb2HLKYLqtgYrNJrOIGN2dtZ5fz/vupY9n1LeEqKtLrMsG/lrY5Q8CV1d+Z
#|KsZe0AORGc03BT9IVnyHsgWIz10QVr9JfzhG1qu2x/Bj2vCP3k8RbBQLL5cjVF6pW5OcdEJLx0wyLR5x
#|2qWvhVr5RSZuQQaAetD33qRhkQgSMSO4G7l0phYL/uG9TQZCdMk/my3zzcqxcItQCTFU0T25OUlw46kh
#|31FZeurDNgR0EP+YS0qb8X7xMispNZ5Y+ps/8PJAg7lElkmBbKwbCeSkW/8cbvqii5JqLD9V7+If4P7J
#|Gnv595kXIeeAizsskhF2EL39JPdFFWGijBYZm1DbeENIVafSaOSSvlpnX3HMzuWnrl+tBuOiJh0jv980
#|WpeoAX9opSZXC5E6wp2IX6dr5y7UnALQje5umxuM5Ps4lRHQg96rR6yr3utzgg0zcwyFxxpkBscrq3u9
#|5ZBHrY4y5UbykSf1eoO1pEr61TkR0oLKryL/9P1QDnR+FCNwM81xbPlbjRbH4kBvJLX76etcdkUnCWf1
#|gQK+Ska2p760HmEt2kBhValRe99yF4u3maIoONxYuG4v6mR0ib1jUsPYFPYPKhxgkMrn/6Vu1xOh6h9C
#|FbEhvj3mOvKUAG1usWSdHQdVZlhTmMuFXMSVz+RVtAchv6W6TG6ktVTrzuQwbOH3nC07R28n2LhFCsXU
#|LxpxSWef5KzUAlPYheI3HthSnRWsNr3GsOp2pJXJIz/9g0uQrCRmucXH3Fc46oIFAF/KwLGAnh4o6ui/
#|JX8mWzs08uzdypnT+x6UN8Dvs6S5vYz7A9eB1gacMkjV9nav+ccM6q6Mm09WhX1cUuTm1WNTN1AxeFpp
#|Y2NkUTZueEJlbz0iIGVuY3J5cHRlZEhtYWNWYWx1ZT0iaEdsV3JrR1p1K0ttTUV4U0ZsRG1QbnFJNlB6
#|RHJrWm5zNWJjQmx2NmVwMD0iLz48a2V5RW5jcnlwdG9ycz48a2V5RW5jcnlwdG9yIHVyaT0iaHR0cDov
#|L3NjaGVtYXMubWljcm9zb2Z0LmNvbS9vZmZpY2UvMjAwNi9rZXlFbmNyeXB0b3IvcGFzc3dvcmQiPjxw
#|OmVuY3J5cHRlZEtleSBzcGluQ291bnQ9IjEwMDAwMCIgc2FsdFNpemU9IjE2IiBibG9ja1NpemU9IjE2
#|IiBrZXlCaXRzPSIxMjgiIGhhc2hTaXplPSIyMCIgY2lwaGVyQWxnb3JpdGhtPSJBRVMiIGNpcGhlckNo
#|YWluaW5nPSJDaGFpbmluZ01vZGVDQkMiIGhhc2hBbGdvcml0aG09IlNIQTEiIHNhbHRWYWx1ZT0iV29v
#|WUNRNmhvRFIyZ0VSOXp3RFZLdz09IiBlbmNyeXB0ZWRWZXJpZmllckhhc2hJbnB1dD0iYnB6YWJ3akho
#|K3UrZmx4TXRzcHpoZz09IiBlbmNyeXB0ZWRWZXJpZmllckhhc2hWYWx1ZT0iN0QycnJybmhTWXAvNjFI
#|Z2o2L3VQaTRHV1paN2lmMGE3K1QvQ1BOMTJLdz0iIGVuY3J5cHRlZEtleVZhbHVlPSI4OWwxbHdrTlla
#|QnVrL2tZTmV1R1dBPT0iLz48L2tleUVuY3J5cHRvcj48L2tleUVuY3J5cHRvcnM+PC9lbmNyeXB0aW9u
#|PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
#|
///|
let encrypt_aes_b64 =
#|0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAOwADAP7/CQAGAAAAAAAAAAAAAAABAAAADgAAAAAAAAAAEAAA
#|AgAAAAEAAAD+////AAAAAAAAAAD/////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|///////////////////////////////////////////9//////////7////+////BQAAAAYAAAAHAAAA
#|CAAAAAkAAAAKAAAACwAAAAwAAAANAAAA/v////7/////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|/////1IAbwBvAHQAIABFAG4AdAByAHkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAWAAUA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|/v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAD+////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAP7///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////AAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v///wAAAAAAAAAAAQAAAAIAAAADAAAA/v//////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|////////////////////////////////////////////////////////////////////////////////
#|//////////8DAAIAJAAAAIwAAAAkAAAAAAAAAA5mAAAEgAAAgAAAABgAAAAAAAAAAAAAAE0AaQBjAHIA
#|bwBzAG8AZgB0ACAARQBuAGgAYQBuAGMAZQBkACAAUgBTAEEAIABhAG4AZAAgAEEARQBTACAAQwByAHkA
#|cAB0AG8AZwByAGEAcABoAGkAYwAgAFAAcgBvAHYAaQBkAGUAcgAAABAAAAAo4CLFrKeTKjf5ZIUDZKhs
#|9kAmMAo4s1aL5kf7JzTjiRQAAACzHs4Lxi8EGKGeGW8k5Viv38RyvBDXznGdpnFMBNfB3wAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACESAAAAAAAAbUJcpy+h0iUeSNh0
#|/tUILIIqSptUp6a1IebgVi86XVUpG+f3J+sHlgy2bAyrSIQfHB2wKMvRLiUb1XXO+mIuAG2/kqO/Pmdo
#|+LpQDRaxxgfZKfLnsdCUuMSgffqZce1FX6ooKNJuLbLnOgChhUxU7xPGReN8Wqk0k0cryLfMsK5gy/aC
#|UudnwEa6yHhAhtO+5A/Q8JGt9alYL7vTwnZVpuB2vJI2u1Ja2EQBdjomBSLe3I4RWWBG+iAsZpedpXFj
#|7vtFx35qGoUt6OKsy3ZBn+S7WMRhWk3eSis7T89LgAA6ElmN24QYlI7mg5sPD+yF1j1RCPr0SjMpjneT
#|K6U1zEjya0j4wt+PubMrHuxcdSVdZBpI+xbC20wby2N3FmhroteGkFM1Dbx/dsdakawncIvs8TW+S7I+
#|+4tvecZmxtl/FEkmzJy+Q0w2JIAr/IJRzngarUZdXzFO/cJ8kyld0l4k9OOQc61d8FPMtZsfr6a3pz4A
#|EF+l1tdvNKDBZeLjJTVBuz1DsFW0w+kv+oRaCXPgm0hHOC5u8WwqkVeqc7WofDnP7zgX0OCSP1E4nkRV
#|8pdBdOcR9uqXPuZ0NDpRGUq17D3m9SuUyfJhAnBdK2PqGJ4+OKNzwjigiF4zRTsll05KgcdV5yYf6fdl
#|rmKgzzfMT38QPYck6PuZlosc7hVJZuPH0GM9rkVKVg0eLkZMJugzRknLEqqHiMXZZKpbML0Na1zSiS0u
#|GdrlMLJw48Fj5TWdmg5BvfnHadF9nz8WLLqBcQEIcR38N0DRgAqF7wtgD5Py5Qh35bmWmiLV5IHO7bcn
#|+OhBOxfJsHDEm53pT1JRdYGq9+tgULs0VLzCp7wReUFU9FC6UKhsGeoaiFBYO0PhiQpWJtpVM0Nh8goi
#|aAK7VjW9qq/glfnA/F4rikCiVPh/dbBA61+GBxagk39A2SJ8e7hlDhsScZF468HiCwW4yzK6JajUCKGb
#|z7yftlKcdwFjX3/32piwVxS/gs0wTap6pD/7wcjhtvyQMzrkrUM/fBILKJZjTW0odcGsWymPq1F+koqz
#|z3+EE3DvLBt7di2qtHQBwjBN9Vc/QIkHkJOQkJk3U9c2x1xipG0FtCBPzl8njk0FXGVVTk1tPdhk4drz
#|1fkEPKIR6Z5sJPnxjJYIQq1qjvyYPONzg4L+pNiVeqzQDvVcmKJOunQsbWtxj55AWmN1Q4t673/QHO1y
#|Bov9oPRn3A+Hy9JQXKQTFBuw1u8jImUCirMb3HjS1d/y2xL7t6sfnA4UTLR6GWEGO94lR8PgZESbhv6v
#|6wUl8vqWki9/+PvkC+n7GE+xUpF1iRKhYA35GqJHSVk4NSCGR4qaMoT3xgxAaj7JgF11Z/yJk+9kaxsS
#|hcSphxxh70tgrKtFzWfxqgGbB2raDysCM3Z+Q7zijcZGrbANh6EKXBI/KyROBN1TCGlU9XopuHD5pxM3
#|jgDfwEf+7vqxQYK0hx08LHghmAMLE8fRTz5Ya7T6VSQqMb5/apTbcfh7rJIX1kfhKP8LyF3OKiSrnZVa
#|MtYuBnQ2QkfMzKZgbV/6i++UAoMUDJjP1JIdOLSpbu+niU8FgI/zb8frqV1+EiCclrtK7lcnMqL6HNjv
#|V+nhW+M3EvwO3dbmzBk5kB4WBQM7uDvHFI/BFN6StRA6aMPDCukxRWwI1Y2tKpswq8l98anz/rWbamMJ
#|9dcCIt6in21+aOeD4cYoWIvBlHhoezw9Q46WTeCoIrFKKAjsEZ2CePRX9/6+0HsEkfwTQ4Qt32Bpr6Fr
#|Wwy/yVl87I6TrvOR1IJ2USyIBYsJe5LaEh3UqjfBH1MJQQCADMkwm5W6aMWAZs77nm1LwsVmjXP2nUbs
#|pXoj2UgAAU7ItLlMoPctFK20Hk2uzZPBzH/6TDuPQdpNVTTrFTHEvCe8wg5ckUCRdCpo25zPUFe/3M8v
#|tcbcBB1MDXW9RlE6RuzpdtkOFGmOPsUNU3ukcvAAGa8W9q+0FlIjzQ9zoJLcSzKekf+PiioYiZBCU4Gw
#|ltPpjfm0vXu8Ei/TnO7xPTubo/z08ukB1zqtGjqE+1VpDPrZJ+um2xSqIYmWL1uEwC3OLX15SIgXFb7U
#|LtmVTYB5OpCbqslzLIdoLGsDAOk0035Uzmp3vRhE7UYJVXXsYT8sAmunDEIkzxUsmtcUMEx5mwaIpTDO
#|JhORySsr0xE2W9f4f54HIWJYb0IvzQtM2cJbiQsSQUm3Z7C6NtcOisRw7ZRpGm021XlQGkuPBSL6Cqvj
#|E/vO3BP+n1qHb/6UAURpz+uJqSVqi6Hm/fNc9js6MmTwN2WEvx2I3e2mgptNfgNevrV9o3aQGqjP+haI
#|iiwzm+N1UdA0MXapJGhWyWcPxW+sRg5utFZqzLN61cCG8i8+lECElRoDJcv143Fmws/CnGf+6B+1zY3K
#|sb6ygXGyl8eqE7eAy9oCuO/+BP5sndhbvIdrFJOLL7OBqR+ZqSydZ+larO/CblUQbpfqVV6i9RtXy1zx
#|nZHOnSOmj20GtcKRPLNsrfoaXPbYMP0aAbWU3KACWur7VRN/tgALplyLKnJDuhPAmG2ONDEqIK2ENxKg
#|fIDW5MUSTmSZoLrdF6xiWsQZmhX4h2h1LPSAwmX+7p0Jaf9bKKAiUUkHcopufGNij2Ug33Gm663BpPJx
#|8SO5GfOCt0cbV3pYAdCOGRuednyFqnIvOza5FZMZVYHlcUKRUqbevf13xGZBX/LoAiDsCfxAkdYAcwrp
#|LqwoktSWifvWrmUIqWWdZkfqJBs6Psatu6HHWExQ76Qy+3vNPXkyP5vOqlIU9y3HD/9nkBaYcXBB9kbw
#|4xyzIpKyvXIWb84uAjbwpEzBk2eINphYWath02oQOs7KH0vFgcZ7BDJYrzh2g4BqYFrHT1xT5sIG6j/f
#|vBj/1gRZ60ZuNpqvCR8cvnN9dAvogDNxStb3n9FICy17warNFq+9P2aPyVO0/WbOH7Nys1aGTwUMhRDd
#|2opfvfom6EeRdiyTkN9IPa48ifvJPJLULtsmzVBxoy8PFQC6vkjOyA3FYGpGClYfJWOWf2csiy0qU3i5
#|d3eIdK2IEMSQqcymOFx8rapxo2XPoyqmcliDsw1WHOKamnFXYMqpVsPEs1tC4Nzp8FIML0QjnWuoG1Jh
#|Yox72p4r60Rpd6vhB5Ne+JMDeJzpyH5nKZk4Ssz0o+tK++tjDcSAuhFeg9wV2hTq6L4sLqiSnlp8Uf1E
#|+3vxGSt8b3VT7mQ8Qd9Ot5uYpZdjQDP+i7YM8ttl9mKX043hXOoxNRz06mkc+A2pMqV3SUsGY1QMtQdi
#|Q8yxDMMoKJXP3QRdxlB5q/PNzkqB8USvO3QCUUZ+OKbomQ8ULv3Wa3YReI0HMqD7Bi04kTnCG+NQsB+V
#|UIW44Mn1NUaXNvxX2cfPVxy3qdAiq7IuV0JSVoP95nPj7RdpDRU/6XCEzER5SiTYXvU/izLPZsYkzyyG
#|aIjR15joiHTYaPj9ISqKR5VDch6gstWVboqOQFQ00XagQxlPonn/C+MBm0ZvGoK2r8semj6hyXaoLmkc
#|R6lYwmG0x7vgFL5Bf896dhC8mb0bQJNrT+a3sUxTTAtZJxALZ2KGKxmjxj2uXq1j0Si01fcKDtLFvHQr
#|h2gxVwgMCxhQyWYLbsCDi4669Mj2kj9fq+aGYKJGjG3a5lLi3URGFQhFTwChL0HT4hGuhlHyPWqtF+wf
#|1nqN8BZafkS38mAMx3StowYU8FYjYolPS2EVoHVPoa07vZhwySLzxuWRzksDVk7BZ2QSi0QcMPIygZD+
#|R4GAEthSI5u64VuyM2VnGU+QDx9cfB980fLLagOi4/upUZmF9bNk1bkATTOjX91drPWm2p7YeslVYouT
#|IDll1EJUbhk9PJpJKbyT6rSF5HTPLqDxmnsH0ppvMdpsxgK0ndyuau2NlPjBGR+e9PNmQidoUPsLRpW+
#|7n9ft0pyoNhJ5DdVz26gRlsVlwDq48vJoJ6Db7KfW2Rx9VN+650Ilq2xvOU73dAn7iWOL7Zqcig7h3MF
#|Sw5VjeoTw9vRPXCt739cf4N1DIMJZ1mTC+9FJSA5s6ZdvlL9P9ZhHt3b4KuTksy5+sjsDpSqjI3zMdVJ
#|JxiopBtm9Crbz5ZzikmBPEGyXl/ey1UJ2inKrEWlOJlI1bZmIftBWQ/jpDX9qOp/DbsI+2MIv56h3OCj
#|VpLNfJwErT5wv4swqDTIMU7GllxKF4S3ouw1sNGTCM+HhnmqYd+aJH70eU28d2P3BR9w6GLYBJmXEWYs
#|Cy0242Z8lVc/AnjPOw42mHhMH/5QHQU1XN9wmtF3rFuhafqK8ZHn8CS7H+9T66k07k/o+RKfFhnaKl6e
#|B4ZDCT+8vCNn8lid4A9l2cyRy1zzx+Ve6TJ9NB+c/CN4P7AruB9k1g5RhmQ8lI0ejI+QeGXNIJo5P7SO
#|8LFPGLIzL+t/9stp6pZ2EaBI5d2psFb6h5tToL9cf88DU8HM0a4aIJwC4tJOlEHHENE6VSy87gTQGY0g
#|+XZ4UFFBrCzn4z7kFdtm8kLaPiFPY5T5oRCXK7iO7oPInN9S2UYOv2pho+PeczslWE4XkfO/gRZx3Oxc
#|0lGfoGNtSbJ7QBau8QHdBvkecbcdtchhV/KitB5lDRgploThPJvdHUa3hejVZR4+u649WCnAavNr9rIc
#|kmehOJzbZ56gusl40RB6InVW/fjWZO8AhGNSeyvkJfDLKqQZfrc3fKCmqFr2ErrVWylRfABulcok+zTj
#|8+j9VlYmEOONbNi/IyoFDe4Y8kWQPjCfzSZcATg/rVteXqP4Pb3LikGQvSB8Lb/wu2hLPYpcoE7wHD4h
#|/5G0RczTlbI1ciJkOBCo/XOqgRIZugs+vCU5Ci83DYCxzULO32Wc+XmDFMl5o8SVDt0ogFx9BVjsZhV/
#|cLOeFIcC/JaKJv7wN2SjXMENVnc0AgRBx1lpbz0MFthCkr4jGR/c5isUZ8jYWMYU6PJGtXgjVS+xzo+C
#|GWBde9ywcIR1dsmmPZRt1TWL/6KaRD30xAfETVpOZG0ciqjZLUuaL17snvJKMOOzvHGoEYci4Jqf9W66
#|KgtybiZKUZzyMy6iSKpabPZu96figx3jNCPfWsvTvSxykr+7ejklEFBEdrTz9vVeOdBRVge2BbSSJF5E
#|OzaJYVEwLg+MxTWQ0cWdzSsn2Lgd0SWhrjFPsBx18pPekPEBAFUGAMAdu3lFVtU3LI6kJoTsumK9UTg9
#|X3PPQM5T23kYpN6WTC+JhhNousJyj16DgOJvQlsRJGCQONbBpYTnRqbqnHZFvh9URGcYfa6QUhdjE6mb
#|4omx4ZX6lnQpXcVpiJ4EiVSg7w55hX6bNr9+4+ahVv1LSm74f7eAEYbitR6hPt08POxjfy5OMZdSJhp/
#|x1zy9O049dWd9ouelXSI4xzoUYeA502uMNv2zVsV/I8CiIaEdMVpXY/w5DK3rKUY0GTU8/ckCEEskDuv
#|TnoYBl9v6Is8Z7XeV8afs47d04uLjRp8TcoBRSuoKqKe1AQrCruiXay0WL8G9KqIVp7oFX1OR8Bgt82K
#|EuarfyVEflWji+SMD5bYNYNGhnreEIK5FRWLjHRv3Iv+z758EVsK/wVa1AZtu1RIKWmfPX5veyQSa4U8
#|5kcZ+Bx0ui+UU4+f1iQILAldcgQBnV7FsXOwuTOVZTE4fcz2fvamzD416IfKjDJI4hCQlKzNISxfqLZ4
#|22nCIu16O4NSEFwvEB468l9v6Is8Z7XeV8afs47d04s7l9lFcJE84FDvlcfjQhRqoLMI09UvmKEDt6JR
#|3HhpRJ8ZKHnPAz4y4Sle2nM0QXarXKcqvdtqfu/be1QMicCH21WHs9lZcKwT+ir+FfJRahUKhLcPtOtS
#|/Tiw3dS7FMKTT5bU20uI4d0sDnyrPoDKFBlE31BfpD+Ayyeff2uVcgPcw4wsQFOyIXQxf4UbNcJs7Alq
#|3tDv/iwJ4vQoKywCgOQjFwXwHupPsA5/Odx5EzWSii/N9bAsnkz/ixTyFHVU7CCrKtmly0YWy9RAI0LY
#|j/zGG8wEsaVt3fwIAzI7u2rp+LaaBt/pJXzc1DqJBNW1aXqyqWA5kM3hzd+O9NEKIVkmvmJNBikbu/rc
#|VwErDGcbYKiCW5Ox7PHmuviMaowd4YITk4eJEPMN067pxBIL1/SexonPPdugMo1Qy0KcPOLpZyg5l8l0
#|isSVDuQEacWcNiJBjbC1bNLKfrDR2pTyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|UgBvAG8AdAAgAEUAbgB0AHIAeQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAABYABQD//////////wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAA
#|AAEAAAAAAABFAG4AYwByAHkAcAB0AGkAbwBuAEkAbgBmAG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAHgACAP////8CAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAADgAAAAAAAAAEUAbgBjAHIAeQBwAHQAZQBkAFAAYQBjAGsAYQBnAGUAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAiAAIA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAABAAAADgSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////8AAAAAAAAAAAAAAAAAAAAA
#|AAAAAAAAAAAAAAAAAAAAAAAAAAD+////AAAAAAAAAAA=
#|
///|
test "encryption info detection" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
inspect(bytes.length(), content="14336")
let reader = cfb_read(bytes) catch {
error =>
fail(
"Agile CFB parse failed: \{repr(error)}; FAT=\{read_u32_le(bytes, 0x2c)}, sectorShift=\{read_u16_le(bytes, 0x1e)}",
)
}
let info = reader.read_stream("EncryptionInfo") catch {
error => fail("Agile encryption-info read failed: \{repr(error)}")
}
let is_agile = match encryption_mechanism(info) {
Agile => true
Standard => false
}
inspect(is_agile, content="true")
let xml = @encoding/utf8.decode(info[8:])
inspect(xml.contains(" {
kdf_checks[0] += 1
kdf_checks[0] >= 3
}),
) catch {
error => Err(error)
}
assert_true(kdf_result is Err(ReadCancelled))
assert_true(kdf_checks[0] >= 3)
let aes_checks = [0]
let aes_result : Result[Bytes, Error] = Ok(
aes_ecb_decrypt_cancellable(
Bytes::make(16, b'k'),
Bytes::make(16 * 1024, b'v'),
() => {
aes_checks[0] += 1
aes_checks[0] >= 3
},
),
) catch {
error => Err(error)
}
assert_true(aes_result is Err(ReadCancelled))
assert_true(aes_checks[0] >= 3)
}
///|
test "encryption KDF work and AES key sizes are validated before derivation" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let limits = ReadLimits::with_values(max_kdf_iterations=1)
let bounded : Result[DecryptedPackage, Error] = Ok(
decrypt_encrypted_package_result(bytes, "password", limits~),
) catch {
error => Err(error)
}
match bounded {
Err(ResourceLimitExceeded(kind~, limit~, actual~)) => {
inspect(kind, content="kdf_iterations")
inspect(limit, content="1")
inspect(actual, content="100000")
}
_ => fail("expected Agile KDF work limit")
}
let standard_bounded : Result[DecryptedPackage, Error] = Ok(
decrypt_encrypted_package_result(
decode_base64_multiline(encrypt_aes_b64),
"password",
limits~,
),
) catch {
error => Err(error)
}
match standard_bounded {
Err(ResourceLimitExceeded(kind~, limit~, actual~)) => {
inspect(kind, content="kdf_iterations")
inspect(limit, content="1")
inspect(actual, content="50000")
}
_ => fail("expected Standard KDF work limit")
}
let invalid_key : KeyData = {
salt_size: 16,
block_size: 16,
key_bits: 2_147_483_640,
hash_size: 20,
cipher_algorithm: "AES",
cipher_chaining: "ChainingModeCBC",
hash_algorithm: "SHA1",
salt_value: Bytes::make(16, b's'),
}
let key_result : Result[Unit, Error] = Ok(validate_key_data(invalid_key)) catch {
error => Err(error)
}
inspect(
key_result is Err(InvalidEncryptionInfo(msg="invalid AES key size")),
content="true",
)
}
///|
test "standard AES verifier requires the complete SHA-1 digest" {
let verifier = Array::make(72, (0x00).to_byte())
// VerifierHashSize is a little-endian u32 at offset 36. A one-byte prefix
// must not be accepted as password proof.
verifier[36] = (0x01).to_byte()
let result : Result[Bool, Error] = Ok(
standard_password_verified(
Bytes::make(16, (0x00).to_byte()),
Bytes::from_array(verifier),
"AES",
),
) catch {
error => Err(error)
}
inspect(
result is Err(InvalidEncryptionInfo(msg="invalid verifier hash size")),
content="true",
)
}
///|
test "Agile verifier fallback reuses a single bounded archive" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let reader = cfb_read(bytes)
let info = reader.read_stream("EncryptionInfo")
let encrypted_package = reader.read_stream("EncryptedPackage")
let xml = @encoding/utf8.decode(info[8:])
let verifier = match attr_value(xml, "encryptedVerifierHashValue") {
Some(value) => value
None => fail("missing encrypted verifier hash")
}
let changed_first = if verifier.has_prefix("A") { "B" } else { "A" }
let changed_verifier = changed_first + verifier[1:].to_owned()
let patched_xml = xml.replace_all(
old="encryptedVerifierHashValue=\"\{verifier}\"",
new="encryptedVerifierHashValue=\"\{changed_verifier}\"",
)
let patched_info : Array[Byte] = info[:8].to_array()
patched_info.append(@encoding/utf8.encode(patched_xml).to_array())
let builder = cfb_builder_new()
builder.put("EncryptionInfo", Bytes::from_array(patched_info))
builder.put("EncryptedPackage", encrypted_package)
let workbook = read_with_password(builder.write(), "password")
debug_inspect(
workbook.get_cell_value("Sheet1", "A1"),
content="Some(\"SECRET\")",
)
}
///|
test "encryption info standard detection" {
let bytes = decode_base64_multiline(encrypt_aes_b64)
inspect(bytes.length(), content="8192")
let reader = cfb_read(bytes)
let info = reader.read_stream("EncryptionInfo")
let is_standard = match encryption_mechanism(info) {
Standard => true
Agile => false
}
inspect(is_standard, content="true")
}
///|
test "read encrypted agile workbook" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let workbook = read_with_password(bytes, "password")
let sheet = match workbook.sheet("Sheet1") {
Some(value) => value
None => fail("missing Sheet1")
}
debug_inspect(sheet.get_cell("A1"), content="Some(\"SECRET\")")
}
///|
test "read encrypted standard workbook" {
let bytes = decode_base64_multiline(encrypt_aes_b64)
let workbook = read_with_password(bytes, "password")
inspect(workbook.sheets().length() > 0, content="true")
}
///|
test "decrypt agile zip read" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let decrypted = decrypt_encrypted_package(bytes, "password")
let result : Result[@zip.Archive, Error] = Ok(@zip.read(decrypted)) catch {
e => Err(e)
}
inspect(result is Ok(_), content="true")
}
///|
test "decrypt standard zip read" {
let bytes = decode_base64_multiline(encrypt_aes_b64)
let decrypted = decrypt_encrypted_package(bytes, "password")
let result : Result[@zip.Archive, Error] = Ok(@zip.read(decrypted)) catch {
e => Err(e)
}
match result {
Ok(archive) =>
inspect(archive.get("xl/workbook.xml") is Some(_), content="true")
Err(_) => inspect(false, content="true")
}
}
///|
test "decrypt standard workbook parse" {
let bytes = decode_base64_multiline(encrypt_aes_b64)
let decrypted = decrypt_encrypted_package(bytes, "password")
let result : Result[Workbook, Error] = Ok(read_zip_bytes(decrypted)) catch {
e => Err(e)
}
inspect(result is Ok(_), content="true")
}
///|
test "read encrypted wrong password" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let result : Result[Workbook, Error] = Ok(read_with_password(bytes, "wrong")) catch {
e => Err(e)
}
inspect(result is Err(XlsxError::InvalidPassword), content="true")
}
///|
test "decrypt agile wrong password" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let result : Result[Bytes, Error] = Ok(
decrypt_encrypted_package(bytes, "wrong"),
) catch {
e => Err(e)
}
inspect(result is Err(XlsxError::InvalidPassword), content="true")
}
///|
test "read encrypted without password" {
let bytes = decode_base64_multiline(encrypt_sha1_b64)
let result : Result[Workbook, Error] = Ok(read(bytes)) catch { e => Err(e) }
inspect(result is Err(XlsxError::EncryptedPackage), content="true")
}
///|
test "read_with_password non-encrypted workbook accepts transcoder path" {
let workbook = Workbook::new()
let sheet = workbook.add_sheet("Sheet1")
sheet.set_cell("A1", "Plain")
let bytes = write(workbook)
let transcoder = (_name : String, data : Bytes) => {
@encoding/utf8.decode(data) catch {
_ => raise InvalidXml(msg="transcoder decode failed")
}
}
let parsed = read_with_password(bytes, "ignored", transcoder~)
let parsed_sheet = match parsed.sheet("Sheet1") {
Some(value) => value
None => fail("missing Sheet1")
}
debug_inspect(parsed_sheet.get_cell("A1"), content="Some(\"Plain\")")
}
///|
test "read_with_password non-encrypted workbook without transcoder" {
let workbook = Workbook::new()
let sheet = workbook.add_sheet("Sheet1")
sheet.set_cell("A1", "PlainNoTranscoder")
let bytes = write(workbook)
let parsed = read_with_password(bytes, "ignored")
let parsed_sheet = match parsed.sheet("Sheet1") {
Some(value) => value
None => fail("missing Sheet1")
}
debug_inspect(
parsed_sheet.get_cell("A1"),
content="Some(\"PlainNoTranscoder\")",
)
}
///|
test "write encrypted roundtrip" {
let workbook = Workbook::new()
let sheet = workbook.add_sheet("Sheet1")
sheet.set_cell("A1", "SECRET")
let encrypted = write_with_password(workbook, "passwd")
let decoded = read_with_password(encrypted, "passwd")
let decoded_sheet = match decoded.sheet("Sheet1") {
Some(value) => value
None => fail("missing Sheet1")
}
debug_inspect(decoded_sheet.get_cell("A1"), content="Some(\"SECRET\")")
}
///|
test "write encrypted invalid password length" {
let workbook = Workbook::new()
let long_password = "*".repeat(256)
let result : Result[Bytes, Error] = Ok(
write_with_password(workbook, long_password),
) catch {
e => Err(e)
}
inspect(result is Err(InvalidPasswordLength(_)), content="true")
let read_result : Result[DecryptedPackage, Error] = Ok(
decrypt_encrypted_package_result(b"", long_password),
) catch {
error => Err(error)
}
inspect(read_result is Err(InvalidPasswordLength(_)), content="true")
}
///|
test "standard decryption accepts RC4-style verifier layout" {
// Excelize parses any non-AES AlgID as the RC4-style layout (20-byte
// verifier hash) and still decrypts the package with AES-ECB.
let password = "legacy-pass"
let salt : Array[Byte] = []
for i in 0..<16 {
salt.push(i.to_byte())
}
let salt_bytes = Bytes::from_array(salt)
// encryption info: version 3.2, flags, header size, 32-byte header
let info = ByteWriter::new()
info.write_u16_le(3) // version major
info.write_u16_le(2) // version minor
info.write_u32_le(0x0000_0024) // flags
info.write_u32_le(32) // header size
info.write_u32_le(0x0000_0024) // header flags
info.write_u32_le(0) // size extra
info.write_u32_le(0x0000_6801) // AlgID: RC4, not an AES id
info.write_u32_le(0x0000_8004) // AlgIDHash: SHA-1
info.write_u32_le(128) // key size bits
info.write_u32_le(1) // provider type
info.write_u32_le(0) // reserved1
info.write_u32_le(0) // reserved2
// RC4-style verifier: salt size + salt + verifier(16) + hash size + hash(20)
info.write_u32_le(16)
info.write_bytes(salt_bytes)
for _i in 0..<16 {
info.buf.push((0xaa).to_byte())
}
info.write_u32_le(20)
for _i in 0..<20 {
info.buf.push((0xbb).to_byte())
}
let info_buf = info.to_bytes()
// package encrypted with the key derived exactly like the reader will
let key = standard_convert_password_to_key(128, salt_bytes, password)
let plaintext : Bytes = b"legacy standard payload!\x00\x00\x00\x00\x00\x00\x00\x00"
let encrypted = aes_ecb_encrypt(key, plaintext)
let package_writer = ByteWriter::new()
package_writer.write_u64_le(24) // real payload length before padding
package_writer.write_bytes(encrypted)
let package_buf = package_writer.to_bytes()
let decrypted = standard_decrypt(info_buf, package_buf, password)
inspect(decrypted.bytes.length(), content="24")
debug_inspect(
@encoding/utf8.decode(decrypted.bytes[:24]),
content=(
#|"legacy standard payload!"
),
)
// truncated RC4-style verifier still raises
let truncated = info_buf[:info_buf.length() - 4]
try standard_decrypt(truncated, package_buf, password) catch {
InvalidEncryptionInfo(msg~) =>
inspect(msg, content="invalid verifier block")
e => fail("unexpected error \{repr(e)}")
} noraise {
_ => fail("expected truncated verifier to fail")
}
}