Skip to content

Commit

Permalink
Merge pull request #42 from Veinar/feature/sha
Browse files Browse the repository at this point in the history
Feature/sha
  • Loading branch information
Veinar authored Nov 26, 2024
2 parents 013b293 + e10643f commit e826dee
Show file tree
Hide file tree
Showing 12 changed files with 378 additions and 55 deletions.
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

0 comments on commit e826dee

Please sign in to comment.