File size: 7,823 Bytes
8959b8d
 
 
 
 
 
 
 
 
 
c93b3cb
8959b8d
 
 
 
 
 
 
7e95396
8959b8d
 
 
 
 
 
 
 
 
 
 
 
 
7e95396
8959b8d
 
7e95396
 
8959b8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e95396
 
8959b8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e95396
8959b8d
 
 
 
 
 
 
 
 
 
 
 
 
 
7e95396
8959b8d
7e95396
 
8959b8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import os
import io
import gradio as gr
import json
import tempfile
import zipfile
from PIL import Image

# --- Required Core Logic ---
# Ensure keylock1 and repo_to_md are in the same directory or accessible in the Python path.
from keylock import core as keylock_core
from repo_to_md.core import markdown_to_files

# --- Helper Functions for the Decoder UI ---

def retrieve_and_process_files(stego_image_pil: Image.Image, password: str):
    """
    Extracts data from an image and prepares the file browser UI components for update.
    This version handles cases where the password is an empty string.
    """
    # Initial state for all UI components
    status = "An error occurred."
    file_data_state, file_buffers_state = [], {}
    file_browser_visibility = gr.update(visible=False)
    filename_choices = gr.update(choices=[], value=None)
    accordion_update = gr.update(label="File Content")
    code_update = gr.update(value="")

    if stego_image_pil is None:
        status = "Error: Please upload or paste an image."
        return status, file_data_state, file_buffers_state, file_browser_visibility, filename_choices, accordion_update, code_update

    # The check for a mandatory password has been removed. An empty string is now a valid input.

    try:
        # Step 1: Extract and decrypt data from the image.
        # The password (which can be empty) is passed directly to the core function.
        extracted_data = keylock_core.extract_data_from_image(stego_image_pil.convert("RGB"))
        decrypted_bytes = keylock_core.decrypt_data(extracted_data, password)
        
        # Step 2: Decode the extracted data (JSON or plain text)
        try:
            data = json.loads(decrypted_bytes.decode('utf-8'))
            md_output = data.get('repo_md', json.dumps(data, indent=2))
            status = f"Success! Data extracted for repo: {data.get('repo_name', 'N/A')}"
        except (json.JSONDecodeError, UnicodeDecodeError):
            md_output = decrypted_bytes.decode('utf-8', errors='ignore')
            status = "Warning: Decrypted data was not valid JSON, but was decoded as text."
        
        # Step 3: Process the markdown content into files
        file_data, file_buffers = markdown_to_files(md_output)
        if isinstance(file_data, str):
             status = f"Extraction successful, but file parsing failed: {file_data}"
             return status, [], {}, file_browser_visibility, filename_choices, accordion_update, code_update

        # Step 4: Prepare the UI updates
        file_data_state, file_buffers_state = file_data, file_buffers
        filenames = [f['filename'] for f in file_data]
        
        if filenames:
            file_browser_visibility = gr.update(visible=True)
            filename_choices = gr.update(choices=filenames, value=filenames[0])
            first_file = file_data[0]
            accordion_update = gr.update(label=f"File Content: {first_file['filename']}")
            code_update = gr.update(value=first_file['content'])
        else:
            status += " (No files were found in the decoded content)."

    except ValueError:
        status = "Error: Failed to decrypt. This usually means the password is wrong (or was required but not provided)."
    except Exception as e:
        status = f"An unexpected error occurred: {e}"
    
    return status, file_data_state, file_buffers_state, file_browser_visibility, filename_choices, accordion_update, code_update


def update_file_preview(selected_filename, file_data):
    """Updates the file content preview when a new file is selected from the dropdown."""
    if not selected_filename or not file_data:
        return gr.update(label="File Content"), gr.update(value="")
    
    selected_file = next((f for f in file_data if f['filename'] == selected_filename), None)
    
    if selected_file:
        return gr.update(label=f"File Content: {selected_file['filename']}"), gr.update(value=selected_file['content'])
    
    return gr.update(label="File Content"), gr.update(value="Error: File not found in state.")


def download_all_zip(buffers):
    """Creates a zip file from the file buffers and returns its path for download."""
    if not buffers:
        return None
    
    with tempfile.NamedTemporaryFile(delete=False, suffix=".zip", prefix="decoded_files_") as tmp:
        with zipfile.ZipFile(tmp.name, "w", zipfile.ZIP_DEFLATED) as zf:
            for filename, content in buffers.items():
                zf.writestr(filename, content)
        return tmp.name


# --- GRADIO UI DEFINITION ---
with gr.Blocks(theme=gr.themes.Soft(), title="KeyLock Decoder") as demo:
    gr.Markdown("# KeyLock Image Decoder")
    gr.Markdown("Upload or paste your KeyLock image, enter the password (if one was used), and click 'Extract Files' to view and download the contents.")
    
    # State variables to hold data between user interactions
    file_data_state = gr.State([])
    file_buffers_state = gr.State({})

    with gr.Row():
        with gr.Column(scale=1):
            # --- INPUTS ---
            extract_stego_image_upload = gr.Image(
                label="Upload or Paste KeyLock Image",
                type="pil",
                sources=["upload", "clipboard"]
            )
            extract_password_input = gr.Textbox(
                label="Decryption Password (Optional)",
                type="password",
                placeholder="Leave blank if no password was set",
                info="If a password was used during encryption, it is required here."
            )
            extract_button = gr.Button(
                value="Extract Files",
                variant="primary"
            )
            
        with gr.Column(scale=2):
            # --- OUTPUTS ---
            extract_output_status = gr.Textbox(
                label="Extraction Status",
                interactive=False,
                placeholder="Status messages will appear here..."
            )
            
            with gr.Column(visible=False) as file_browser_ui:
                gr.Markdown("--- \n ### Decoded Files")
                download_all_zip_btn = gr.Button("Download All as .zip")
                
                file_selector_dd = gr.Dropdown(
                    label="Select a file to preview",
                    interactive=True
                )
                with gr.Accordion("File Content", open=True) as file_preview_accordion:
                    file_preview_code = gr.Code(
                        language="markdown", # Will auto-detect based on file in many cases
                        interactive=False,
                        label="File Preview"
                    )
                
                # Hidden component to handle the zip download
                download_zip_output = gr.File(
                    label="Download .zip file",
                    interactive=False
                )

    # --- EVENT HANDLERS ---
    
    extract_button.click(
        fn=retrieve_and_process_files,
        inputs=[
            extract_stego_image_upload,
            extract_password_input
        ],
        outputs=[
            extract_output_status,
            file_data_state,
            file_buffers_state,
            file_browser_ui,
            file_selector_dd,
            file_preview_accordion,
            file_preview_code
        ]
    )

    file_selector_dd.change(
        fn=update_file_preview,
        inputs=[
            file_selector_dd,
            file_data_state
        ],
        outputs=[
            file_preview_accordion,
            file_preview_code
        ]
    )

    download_all_zip_btn.click(
        fn=download_all_zip,
        inputs=[file_buffers_state],
        outputs=[download_zip_output]
    )

if __name__ == "__main__":
    demo.launch(debug=True)