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

Feature/sha #42

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 29 additions & 5 deletions envcloak/commands/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@
required=False,
help="Path to save the comparison result as a file.",
)
@click.option(
"--skip-sha-validation",
is_flag=True,
default=False,
help="Skip SHA-256 integrity validation checks during decryption.",
)
@debug_option
def compare(file1, file2, key1, key2, output, debug):
def compare(file1, file2, key1, key2, output, skip_sha_validation, debug):
"""
Compare two encrypted environment files or directories.
"""
Expand Down Expand Up @@ -91,8 +97,18 @@ def compare(file1, file2, key1, key2, output, debug):
if Path(file1).is_file() and Path(file2).is_file():
debug_log("Debug: Both inputs are files. Decrypting files.", debug)
try:
decrypt_file(file1, file1_decrypted, key1_bytes)
decrypt_file(file2, file2_decrypted, key2_bytes)
decrypt_file(
file1,
file1_decrypted,
key1_bytes,
validate_integrity=not skip_sha_validation,
)
decrypt_file(
file2,
file2_decrypted,
key2_bytes,
validate_integrity=not skip_sha_validation,
)
except FileDecryptionException as e:
raise click.ClickException(f"Decryption failed: {e}")

Expand Down Expand Up @@ -141,9 +157,17 @@ def compare(file1, file2, key1, key2, output, debug):
file2_decrypted, filename.replace(".enc", "")
)
try:
decrypt_file(str(file1_path), file1_dec, key1_bytes)
decrypt_file(
str(file2_files[filename]), file2_dec, key2_bytes
str(file1_path),
file1_dec,
key1_bytes,
validate_integrity=not skip_sha_validation,
)
decrypt_file(
str(file2_files[filename]),
file2_dec,
key2_bytes,
validate_integrity=not skip_sha_validation,
)
except FileDecryptionException as e:
raise click.ClickException(
Expand Down
19 changes: 16 additions & 3 deletions envcloak/commands/decrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@
@click.option(
"--key-file", "-k", required=True, help="Path to the decryption key file."
)
def decrypt(input, directory, output, key_file, dry_run, force, debug):
@click.option(
"--skip-sha-validation",
is_flag=True,
default=False,
help="Skip SHA3 integrity validation checks during decryption.",
)
def decrypt(
input, directory, output, key_file, dry_run, force, debug, skip_sha_validation
):
"""
Decrypt environment variables from a file or all files in a directory.
"""
Expand Down Expand Up @@ -122,7 +130,7 @@ def decrypt(input, directory, output, key_file, dry_run, force, debug):
f"Debug: Decrypting file {input} -> {output} using key {key_file}.",
debug,
)
decrypt_file(input, output, key)
decrypt_file(input, output, key, validate_integrity=not skip_sha_validation)
click.echo(f"File {input} decrypted -> {output} using key {key_file}")
elif directory:
input_dir = Path(directory)
Expand All @@ -141,7 +149,12 @@ def decrypt(input, directory, output, key_file, dry_run, force, debug):
f"Debug: Decrypting file {file} -> {output_file} using key {key_file}.",
debug,
)
decrypt_file(str(file), str(output_file), key)
decrypt_file(
str(file),
str(output_file),
key,
validate_integrity=not skip_sha_validation,
)
click.echo(
f"File {file} decrypted -> {output_file} using key {key_file}"
)
Expand Down
95 changes: 85 additions & 10 deletions envcloak/encryptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import click
from click import style
from envcloak.exceptions import (
InvalidSaltException,
InvalidKeyException,
EncryptionException,
DecryptionException,
FileEncryptionException,
FileDecryptionException,
IntegrityCheckFailedException,
)
from envcloak.constants import NONCE_SIZE, KEY_SIZE, SALT_SIZE
from envcloak.utils import compute_sha256


def derive_key(password: str, salt: bytes) -> bytes:
Expand Down Expand Up @@ -76,12 +80,13 @@ def encrypt(data: str, key: bytes) -> dict:
raise EncryptionException(details=str(e)) from e


def decrypt(encrypted_data: dict, key: bytes) -> str:
def decrypt(encrypted_data: dict, key: bytes, validate_integrity: bool = True) -> str:
"""
Decrypt the given encrypted data using AES-256-GCM.

:param encrypted_data: Dictionary containing ciphertext, nonce, and tag.
:param key: Decryption key (32 bytes for AES-256).
:param validate_integrity: Whether to enforce integrity checks (default: True).
:return: Decrypted plaintext.
"""
try:
Expand All @@ -95,45 +100,115 @@ def decrypt(encrypted_data: dict, key: bytes) -> str:
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()

if validate_integrity:
# Validate plaintext hash if present
if "sha" in encrypted_data:
sha_hash = compute_sha256(plaintext.decode())
if sha_hash != encrypted_data["sha"]:
raise IntegrityCheckFailedException(
details="Integrity check failed! The file may have been tampered with or corrupted."
)

return plaintext.decode()
except Exception as e:
raise DecryptionException(details=str(e)) from e


def encrypt_file(input_file: str, output_file: str, key: bytes):
"""
Encrypt the contents of a file and write the result to another file.

:param input_file: Path to the plaintext input file.
:param output_file: Path to save the encrypted file.
:param key: Encryption key (32 bytes for AES-256).
Encrypt the contents of a file and write the result to another file,
including SHA-256 of the entire encrypted JSON structure.
"""
try:
with open(input_file, "r", encoding="utf-8") as infile:
data = infile.read()

# Encrypt plaintext
encrypted_data = encrypt(data, key)

# Compute hash of plaintext for integrity
encrypted_data["sha"] = compute_sha256(data)
print(
f"Debug: SHA-256 hash of plaintext during encryption: {encrypted_data['sha']}"
)

# Compute hash of the entire encrypted structure
file_hash = compute_sha256(json.dumps(encrypted_data, ensure_ascii=False))
encrypted_data["file_sha"] = file_hash # Store this hash in the structure
print(
f"Debug: SHA-256 hash of encrypted structure (file_sha): {encrypted_data['file_sha']}"
)

with open(output_file, "w", encoding="utf-8") as outfile:
json.dump(encrypted_data, outfile, ensure_ascii=False)
except Exception as e:
raise FileEncryptionException(details=str(e)) from e


def decrypt_file(input_file: str, output_file: str, key: bytes):
def decrypt_file(
input_file: str, output_file: str, key: bytes, validate_integrity: bool = True
):
"""
Decrypt the contents of a file and write the result to another file.
Decrypt the contents of a file and validate SHA-256 integrity for both
the plaintext and the encrypted file.

:param input_file: Path to the encrypted input file.
:param output_file: Path to save the decrypted file.
:param key: Decryption key (32 bytes for AES-256).
:param key: Encryption key (32 bytes for AES-256).
:param validate_integrity: Whether to enforce integrity checks (default: True).
"""
try:
with open(input_file, "r", encoding="utf-8") as infile:
encrypted_data = json.load(infile)

decrypted_data = decrypt(encrypted_data, key)
if validate_integrity:
# Validate hash of the entire encrypted file (excluding file_sha)
expected_file_sha = encrypted_data.get("file_sha")
if expected_file_sha:
# Exclude "file_sha" itself from the recomputed hash
data_to_hash = encrypted_data.copy()
data_to_hash.pop("file_sha")
actual_file_sha = compute_sha256(
json.dumps(data_to_hash, ensure_ascii=False)
)
# print(f"Debug: Stored file_sha: {expected_file_sha}")
# print(f"Debug: Computed file_sha: {actual_file_sha}")
if expected_file_sha != actual_file_sha:
raise IntegrityCheckFailedException(
details="Encrypted file integrity check failed! The file may have been tampered with or corrupted."
)
else:
click.echo(
style(
"⚠️ Warning: file_sha missing. Encrypted file integrity check skipped.",
fg="yellow",
)
)

# Decrypt the plaintext
decrypted_data = decrypt(
encrypted_data, key, validate_integrity=validate_integrity
)

if validate_integrity:
# Validate hash of plaintext
if "sha" in encrypted_data:
sha_hash = compute_sha256(decrypted_data)
# print(f"Debug: Stored sha: {encrypted_data['sha']}")
# print(f"Debug: Computed sha: {sha_hash}")
if sha_hash != encrypted_data["sha"]:
raise IntegrityCheckFailedException(
details="Decrypted plaintext integrity check failed! The file may have been tampered with or corrupted."
)
else:
click.echo(
style(
"⚠️ Warning: sha missing. Plaintext integrity check skipped.",
fg="yellow",
)
)

# Write plaintext to the output file
with open(output_file, "w", encoding="utf-8") as outfile:
outfile.write(decrypted_data)
except Exception as e:
Expand Down
20 changes: 20 additions & 0 deletions envcloak/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,23 @@ class FileEncryptionException(CryptographyException):
"""Raised when file encryption fails."""

default_message = "Failed to encrypt the file."


#### Integrity exceptions
class IntegrityCheckFailedException(CryptographyException):
"""Raised when the integrity check of a file fails."""

default_message = (
"Integrity check failed! The file may have been tampered with or corrupted."
)

def __init__(self, message=None, details=None):
self.message = message or self.default_message
self.details = details
super().__init__(self.message)

def __str__(self):
error_message = f"Error: {self.message}"
if self.details:
error_message += f"\nDetails: {self.details}"
return error_message
11 changes: 11 additions & 0 deletions envcloak/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import hashlib
from pathlib import Path


Expand Down Expand Up @@ -60,3 +61,13 @@ def debug_log(message, debug):
"""
if debug:
print(message)


def compute_sha256(data: str) -> str:
"""
Compute SHA-256 hash of the given data.

:param data: Input data as a string.
:return: SHA-256 hash as a hex string.
"""
return hashlib.sha3_256(data.encode()).hexdigest()
1 change: 1 addition & 0 deletions tests/mock/sha_variables.env.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ciphertext": "DfyoaMMgHDjpbisZHXe+Xn0pIdiIn4TJqBkmKfSSftkPjBa+DIIT5b9+pWIlDTQw7bHF6WfEJdpUIXEcOnIHUTOc2A4E4aVvnhbsAYfAn9d+MwONPDEhoNAw+WkBwrdsHLkAgV5o+EKjaTJ6JEzX+XrBY6zIZ49p5YHT6Mp8rNs/rpMTNIXDBIExoy/xY6fjIT7AgytojXf/rcoOubFdEcedxnQJzn1LTFB3Zqq630MLGZfuNd00jIM3px+5ipxzfZexImRsuQDuy1/PFdWDP8jdMYTUTWMvStTMrwG4JMuzU6cefD/beZ/XmAsSez5Qs/UE//rTibptOX2qYc5FBopzLDrj5Dnad3TQVh1CVjIj+TSXExFv5+Dpk2fvI98ydCj15ym/TpuVXGonuB5K5MeNUmTn9IuViHrnCE04NfBLZb3FObgfDKPaZPeoQa5x+m2Egn7pam8qyFSOHNeeoafg8Nlyapd8YdwiWha4b1dT6nB/IbaMQmtWgyAZ54xKUUAmTGw4rXUmlQKXyq9pIJab9NvBXK/ttzuqwObOHSiibgS4J2/1JEEtH4voFOWMwGS7qEN6fjcXLVPI7KAoEyzhNJTHKf8WueqI2FaSqPeBywFZwvaQo6FHk0PdKAhjtcp7uxFWOOrgXjuU8+90+gPfdDH+Eg8vsTkeFdQxUZWZUHA91+AG2+JHm3l4rQEquVKxDijgy63WZ56PXIoUm0lzqS+rlgU40LNZdOBT5zxfHbhJzem2CYNw6m445vyWXzEdBdDBdvPCb65qPj7HEO4Xor3P9J2xQEEymy11EbH11abQxe53e97xuzghWu7AlY7Ne0ie90ScICKcD9oJV3YhJqjSCXvcbc2/R3AK1J1VqMOZzcf7J6NjO3qKLpbanSRSZD9V08IM1p9/GL/uWgyY50NHnh6w4+OzUQ==", "nonce": "Lm9ZYZz3YmYhyW3U", "tag": "l0sp+HM8qSwpBpjBdt9nbA==", "sha": "13448a667827d62ef7ab673f482b2f2053758e108ba9d455e80a1bdbb56b7b42", "file_sha": "98f5f15ff85e287b04db8d3dfff4ec52fea7d4b453560cd107af4531e47a0cb7"}
1 change: 1 addition & 0 deletions tests/mock/sha_variables.json.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ciphertext": "+IUW/nt8D8OyuhYmxtdl6qcQRW1z1A/WodTXfoZHdzzIwzeQPHu4gGjqHwHlZBMgrUBegfc9EDO9Gu7s+q2oPmJ3xtPqpQOAP0pGUxwKARkymJOtKNmG5DLOlRMTK6x6Itt5LSoXEC0cF5U6pdgUzAPn19I2+MsMhw4zSPmR9Kzj9TdpMp8CAFuD+UGUDtCBCkfJZyb/tidESjk84Ok4iONW5gjlAmWcj9OtZ0rv1SdaVS/nTCE9nA95U8hXknxg6BltgjXn+UiNv5O/KFlgTUU+0cyqDPQsAETppbQ7bnmrqAZNngIQE/2ONZciI6S93wUu/ir+BlkkC0ewTUv6X+7Hd6wP5xQ7IZ9OviJw12HvFL7+0jwhoyMXHXBROVw/kLp/K9SzqSyCaFopHzVX9m9MM3IUdAUc2mXP2B7Xwu7L5mmH9WzXHVUv73YUgjYmCYMDNEEF7F8YRUzmG+sGn8VvzqUThmn1z7617mKB7bmVgtDYUkI5fws1jRBXJ6E4DSJ4eQ4UFSpXAWjTJU8qC2G6PrcxUzJUAwVqmhHyDDVZu1vMhGcc9mE82OYyT4/SatfLx3vqiuZHKGWhVjSCZuBjTITKnaJGK7rcHDf0g9uIGv53UJ8WZ3Q/f9+jeQL6vKulgQzRVrDWcBW99NM9KSKyuKcidJYo+gaYrfXVlSLF7L9vGejkqUWBvbDHAQnlghr7YyaKbVa9q/TxJL3mNNDaKK+GrpItbAW/G78NW5qeAvbWbeXdvZcPo7nNGQqfQH2hkDHvjWErCvUCDEOMBn5u1I53SwE74QDpoWkGsv6Jyz6dRJYck/wZrkrymYg0COL931r6ZRkUuKg/gY0L9S1XK304pt+T0Qi29pq9UNXSTk+vV4ki624GC1AQDj4sOOqcFdL/GES/L2MF7QmdDezBwEDwRaN/lhvh2GsOU3Jkem0tL9q/", "nonce": "vun22UclsVwZ9Q7c", "tag": "6PjtvCeTLNNrx8U3VGLn4Q==", "sha": "d07eb8ad965d8e2b7e126ada0ff1cff69b822c8787fb451314a9fc821e7c43f3", "file_sha": "e58347ed58e640b5948637ee3e346aef5ee20b767c1faab2ad0050393fb8dd02"}
1 change: 1 addition & 0 deletions tests/mock/sha_variables.xml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ciphertext": "tGH9FNZoMzAjGuGrQWhbFQm23Yc0rIauVU/EjnkwDsGENMeTPjGVokgIPoDcEHni86HANSUDhikvsrJAPlZNlZ/wxHoE0ggB2CRhtoR/z2ecJsWJ9K80Z54yQHXhRnY3CEC967Xl23vxPWyT/g1LKhcedY8fm7pNZMI6LxVp0cDJWOfPEwPLSgH9N3bfxDrZLSectd/9+B+p9Vq0KRlukoLl9qZffhennjZNFNHyTzKOG+DrUhakelKiWcwLDH2oVrR1knlWEIoM8ZBvI2e09rqzuofFvTu4+JFhcJaEvLvt8bHlBe0Y93ZXBRMrWkO3Cn9mc36Hdzsw4dVAsxg1fJ0TbPZJr9ArbuxYOjR/XCWFEGmFaD9llYpvhxlrYgjG0ArQy/89Nf1T0/djNOuc8mqghuYM9SZhpQ2qltyqGH49TzZ4wIn0ERc2Spvpxp/tUdq2aAjvOsZtnyz03Ivo5Hyw+3FgJBhJPt/J1LRSkOuLZ7vwd59hjq5EryJrwYgtfBPVQKqkooG77W2Uxii6hli/3ItvjogDyRnCcut9SE4TmS+LqwAF9HV05oRfJ5kPPks+stdnxVy/dNKf5z8+60tMRt9rJ/ofTC5hNirlcSW+E0oiCSLTHOVv3DYsi5dQyUmAT2FCqSG3yUDYuj8frq7frw9RVUG28/AlvqkKwMLo2sTBRTwXra2opSbej6ZwTYq8mLKULD9aPsxuasl99NBgzqlFsAB8XR4vafYSYIYvE3UyVlNbs6SIwZndq2XbwY9erZteDiUf8kxbJz7xR2GB1AVKVT11iBmtT/HmZEShkUHQI7GuXPioQT6UilOGHWLxmcLiCIGc3meMm9gsb/aD1vCPHbdD0+ljIGdyYxXoE3KzLgaDRhO7RAlzdHowY9drcYZk+2hQH8MMGlcORLtRnozKbN2PKvlgSbWcV3JiTjCLd4/Sg2WS0vExe35hQKms2C6mA8so0pNDkVeI3tuIrrfKKvao0xIez+Uxs29ubhmE0vJGzoHIXNnCUR533wr8yhORYHs0jFwY8ZMD5zmUvecpwTJPAJ0i3V7zC+cgeiTW8UFX/kCliM9PqkERhd6LJESHfmmwf6nxj6PHD7nlRxC/UKnBUx02XOLhBCfpFGAn00XUKgrmqOGPXPL4RgtXw3VxtJuMgAR/1mn5269yRYbi3rinb7oDb0+PfX7X66D8EQf6RPN+KwyN1QGAxcHQ8T8sB/mZ0vw6kMKUIrauyIc5FzdP8oWnES3gyGkb5O7z7COwf1U3OudqnFYGlJKcHD8mgR5CTzehlJrG0F0mpHJB/TSc+FNBQ/VdwzHcnRtlwZa0uc+ZEPLPVueZSat7Sr9M3sdKNm/2Aic1joPXw1zbOMHJsIxPexDCpiTAkTvgG13bspCZhMoOktHo+y8FReiJm2NGxalVTiBb/wjnfjP7gkI+80MwJPVrjYlq3F5E+9Bsz2Gn6ypMn4amuA6Sy3v7xzyqMKLoQOG2Z8+0fWrBIYX+2q4jbDt6DQ==", "nonce": "IB/PyrI0AflcQJXP", "tag": "tOyjNYzQxrV5iXsUCGgkEA==", "sha": "caaa3e108f2717fd82c6d955452baa0758da7bcc06d7df4724d7cd4def1662cd", "file_sha": "72fc2710ef006f959b9b020c5e6b154d9245cbea5a14c546a1612f4d92793bc3"}
1 change: 1 addition & 0 deletions tests/mock/sha_variables.yaml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ciphertext": "Pls8pNRxTx6SgdPGGbgz6EU/xiryYJ2CEYmYORMxuBejF8SMnkQXTda2EQdiYW1DO7jAH6Z9YQAUZyy3SrYZcbqge6QvWtC93f/d853XfVBm8qOvepGLUGvoRfMW7urZpjIsfWy4wb4c3T4p7LQp9hyqOzjvyPcu07pHRbJv0hKyDL7VevpSBoM23Kxuw+KRtZ22+HGDn9ZpkzsCv4sq3km6CNgZOH/NH1qCIFayFRrQq0n/riH/ASNnS1bZU4e+kbk1bLDTtyS8ltxBTKABQ08o5WRjT0qpGAuGG6ChAWJrD+0gaoYzIz18zg7DYGL4ck1GwIC2YXEhyCvRwgUeNqP6W9CmaMbKPn2vRo5Vka2B9ViiOApsvws24+nX67gw6vcl9kaJbJRdvIl3vcsTzWBABS+WH9y1kBBSM0859a1fuYzwpig0X0QpCTWmieElRpkKgm/oNXlVXwnstrr3emt0RYwg5vIjpxRsFZKu9qManzs+r+Ga+iVn/umZiLsCiCFUlXAv1D7Rg/m9c7We7bZZVGBTbA+8An8Y4lo4xqRBEBBaVg6dCKHZc6l6JoV6PPF+KdNVq6+W99tUMkAECoEpC4MspDuZ758ZBqIXbqM8oreU/Iz3IeHwxW4hipp/8+LQVddAnOsnQDsiLDXIQ5PgiQhlYvOomvA5Ihy4PyJjQtvC43h//IrACk5x3bBWKCAp6xSke5qVLTgSgrfa1/9s6GQmO5tyKoTtt+h0HyLpIFCtPo8hIvEJulzRTuGIKV5oBy2rNfdwzfOEJ6SKY3g05O6IIRGDtZbP1ZM9nlGfEYNjwTam07wYsEHS/4LygjNZonAGpOCC/8/35qS/OCoZbYnhgkcrMvNbPuVEO3hF4/Nbx23nPMJ7iuwuBkTTdLobrMNaqI05Uj9Vc3m2oxd5OeyiY2v8k1aEMHUOTqzUp3zk6aVawEBdrto=", "nonce": "CoJyLiwgMNZa/025", "tag": "CF5DK+T/7uU6MtEVyyHkHQ==", "sha": "cd96e71dc433d099f50ad7918e4d2237eb5ace783bde4db119ee718a43866dd1", "file_sha": "0497634082406e66b2bb07c29297bd5e93d0090ac1f832de5dc454d8840cdaab"}
1 change: 1 addition & 0 deletions tests/mock/sha_variables_modified.env.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ciphertext": "NI8luEOsn/o9sLiu6C2khuMuY42jV/tz2yNe2qdWv9MxxbOn1HTEXOmLadwklncFjCNMcAly70/xAPiSNPqVgkQj1efwPRyhIhyPS10+QSlXYL28YlsCZK+RvlwEfo2rY9cwiEEMW2VufnUSmTPxhrH9+ZwVg003OA819jkfXA+HIQep67MTzkhYBA5O+t/oQT7EyEJ3vl+LaKaHJEnq1QRG1GbyEPuxG/jt520+2t7qTmqWAsI8b/CxjlX0ZpyzW4o42xQzWbArY6m0cHDeOdgPJhNBstSlJcZP8BgqTBqc046HEUfiUdOkicgx6Si2k1vLG6Tj/nY9z8fCcE5W2kLttq+/S0YyMnPdv5OSzuNZ3MrOqR8JIXCDnOPOAkxt1o27b1808uvPFRAuUwKHAfi7cjcaWPBFRDhSIY9W1E3YSjsevhQzFcqNmKOIUQZZ8kGSsTdCYU8q2CWvkLyDOezqzCmGO8TgYh8KSsEFT+mIWqLTct2mAIWTomTalDko7DSdCcDolY6zW13k4lhZascvuOHHOEThm9CjBFpa47vNfGYuaieB+8bMP8wm0KLOyr203o/OEBlswmv7G5DRrR8M/iJJQ5NvHaJNwNwaM1xMRjGGDh1D5POvo/PqahOBHSJdqcmg6fUsUgDkcs4hC8JncRWKdYjQw5RJfhQn/bvFvAyuGLcGJXfIFi4ByzcmTZTEeIs3q4ghmlLueXyfmCIiSlQe3/g7RkeIbHqozfQoo/dE6UlMmU2UytRn1cilIIabeA48jU+z+6wV6HWmFrx6AAHSdWDjZKhDFqipJ3wT/OKUzIgrCcqviWmimSJLMNZ112aYp+N553aJSo8Bo7QxgcEmjYE+k5jfX7hjDXGqNUOsz1K5207C1u1R5suD1EBJqI7QDsBuZKjzVQax4vJ6qtquDcTJiSy6v9D5ETDXy9d3uxAMvClXsNyDAgr90C/9C89vZiAOdqdQkmaS0jqEb0iIsKdlP1uUZzxhcM2l5lh+P8KUS0F1he3xJxdG3qPwXrY=", "nonce": "QqojJOrWy6QuIYzH", "tag": "kMsOdH1w6BwQxHD90sVSJg==", "sha": "7749d79d08a96e0b0700692f2df7af76313a37fb14eceefec2c39f7ebae73682", "file_sha": "1c9792cbdc260e0748d6ecb21a57662da4b27a4d0871a0472288ed6ba96b9a08"}
Loading
Loading