Spaces:
Runtime error
Runtime error
Commit
·
3d1c35c
0
Parent(s):
lsb steg
Browse files- .gitattributes +1 -0
- .gitignore +2 -0
- .vscode/settings.json +3 -0
- LICENCE +17 -0
- LSBSteg.py +198 -0
- README.md +14 -0
- __init__.py +0 -0
- app.py +89 -0
- requirements.txt +3 -0
- sample-picture.png +3 -0
.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
gradio_cached_examples
|
.vscode/settings.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"python.formatting.provider": "black"
|
3 |
+
}
|
LICENCE
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Copyright © 2017, Robin David - MIT-Licensed
|
2 |
+
|
3 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
4 |
+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
5 |
+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
6 |
+
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
7 |
+
|
8 |
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
9 |
+
|
10 |
+
The Software is provided "as is", without warranty of any kind, express or implied, including but not limited
|
11 |
+
to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall
|
12 |
+
the authors or copyright holders X be liable for any claim, damages or other liability, whether in an action
|
13 |
+
of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other
|
14 |
+
dealings in the Software.
|
15 |
+
|
16 |
+
Except as contained in this notice, the name of the Robin David shall not be used in advertising or otherwise
|
17 |
+
to promote the sale, use or other dealings in this Software without prior written authorization from the Robin David.
|
LSBSteg.py
ADDED
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
# coding:UTF-8
|
3 |
+
"""LSBSteg.py
|
4 |
+
|
5 |
+
Usage:
|
6 |
+
LSBSteg.py encode -i <input> -o <output> -f <file>
|
7 |
+
LSBSteg.py decode -i <input> -o <output>
|
8 |
+
|
9 |
+
Options:
|
10 |
+
-h, --help Show this help
|
11 |
+
--version Show the version
|
12 |
+
-f,--file=<file> File to hide
|
13 |
+
-i,--in=<input> Input image (carrier)
|
14 |
+
-o,--out=<output> Output image (or extracted file)
|
15 |
+
"""
|
16 |
+
|
17 |
+
import cv2
|
18 |
+
import docopt
|
19 |
+
import numpy as np
|
20 |
+
|
21 |
+
channel_order = (2, 1, 0, 3)
|
22 |
+
|
23 |
+
class SteganographyException(Exception):
|
24 |
+
pass
|
25 |
+
|
26 |
+
|
27 |
+
class LSBSteg():
|
28 |
+
def __init__(self, im):
|
29 |
+
self.image = im
|
30 |
+
self.height, self.width, self.nbchannels = im.shape
|
31 |
+
self.size = self.width * self.height
|
32 |
+
|
33 |
+
self.maskONEValues = [1,2,4,8,16,32,64,128]
|
34 |
+
#Mask used to put one ex:1->00000001, 2->00000010 .. associated with OR bitwise
|
35 |
+
self.maskONE = self.maskONEValues.pop(0) #Will be used to do bitwise operations
|
36 |
+
|
37 |
+
self.maskZEROValues = [254,253,251,247,239,223,191,127]
|
38 |
+
#Mak used to put zero ex:254->11111110, 253->11111101 .. associated with AND bitwise
|
39 |
+
self.maskZERO = self.maskZEROValues.pop(0)
|
40 |
+
|
41 |
+
self.curwidth = 0 # Current width position
|
42 |
+
self.curheight = 0 # Current height position
|
43 |
+
self.curchan = 0 # Current channel position
|
44 |
+
|
45 |
+
def put_binary_value(self, bits): #Put the bits in the image
|
46 |
+
for c in bits:
|
47 |
+
val = list(self.image[self.curheight,self.curwidth]) #Get the pixel value as a list
|
48 |
+
if int(c) == 1:
|
49 |
+
val[self.curchan] = int(val[self.curchan]) | self.maskONE #OR with maskONE
|
50 |
+
else:
|
51 |
+
val[self.curchan] = int(val[self.curchan]) & self.maskZERO #AND with maskZERO
|
52 |
+
|
53 |
+
self.image[self.curheight,self.curwidth] = tuple(val)
|
54 |
+
self.next_slot() #Move "cursor" to the next space
|
55 |
+
|
56 |
+
def next_slot(self):#Move to the next slot were information can be taken or put
|
57 |
+
if self.curchan == self.nbchannels-1: #Next Space is the following channel
|
58 |
+
self.curchan = 0
|
59 |
+
if self.curwidth == self.width-1: #Or the first channel of the next pixel of the same line
|
60 |
+
self.curwidth = 0
|
61 |
+
if self.curheight == self.height-1:#Or the first channel of the first pixel of the next line
|
62 |
+
self.curheight = 0
|
63 |
+
if self.maskONE == 128: #Mask 1000000, so the last mask
|
64 |
+
raise SteganographyException("No available slot remaining (image filled)")
|
65 |
+
else: #Or instead of using the first bit start using the second and so on..
|
66 |
+
self.maskONE = self.maskONEValues.pop(0)
|
67 |
+
self.maskZERO = self.maskZEROValues.pop(0)
|
68 |
+
else:
|
69 |
+
self.curheight +=1
|
70 |
+
else:
|
71 |
+
self.curwidth +=1
|
72 |
+
else:
|
73 |
+
self.curchan +=1
|
74 |
+
|
75 |
+
def read_bit(self): #Read a single bit int the image
|
76 |
+
val = self.image[self.curheight,self.curwidth,channel_order[self.curchan]]
|
77 |
+
val = int(val) & self.maskONE
|
78 |
+
self.next_slot()
|
79 |
+
if val > 0:
|
80 |
+
return "1"
|
81 |
+
else:
|
82 |
+
return "0"
|
83 |
+
|
84 |
+
def read_byte(self):
|
85 |
+
return self.read_bits(8)
|
86 |
+
|
87 |
+
def read_bits(self, nb): #Read the given number of bits
|
88 |
+
bits = ""
|
89 |
+
for i in range(nb):
|
90 |
+
bits += self.read_bit()
|
91 |
+
return bits
|
92 |
+
|
93 |
+
def byteValue(self, val):
|
94 |
+
return self.binary_value(val, 8)
|
95 |
+
|
96 |
+
def binary_value(self, val, bitsize): #Return the binary value of an int as a byte
|
97 |
+
binval = bin(val)[2:]
|
98 |
+
if len(binval) > bitsize:
|
99 |
+
raise SteganographyException("binary value larger than the expected size")
|
100 |
+
while len(binval) < bitsize:
|
101 |
+
binval = "0"+binval
|
102 |
+
return binval
|
103 |
+
|
104 |
+
def encode_text(self, txt):
|
105 |
+
l = len(txt)
|
106 |
+
binl = self.binary_value(l, 16) #Length coded on 2 bytes so the text size can be up to 65536 bytes long
|
107 |
+
self.put_binary_value(binl) #Put text length coded on 4 bytes
|
108 |
+
for char in txt: #And put all the chars
|
109 |
+
c = ord(char)
|
110 |
+
self.put_binary_value(self.byteValue(c))
|
111 |
+
return self.image
|
112 |
+
|
113 |
+
def decode_text(self):
|
114 |
+
ls = self.read_bits(16) #Read the text size in bytes
|
115 |
+
l = int(ls,2)
|
116 |
+
i = 0
|
117 |
+
unhideTxt = ""
|
118 |
+
while i < l: #Read all bytes of the text
|
119 |
+
tmp = self.read_byte() #So one byte
|
120 |
+
i += 1
|
121 |
+
unhideTxt += chr(int(tmp,2)) #Every chars concatenated to str
|
122 |
+
return unhideTxt
|
123 |
+
|
124 |
+
def encode_image(self, imtohide):
|
125 |
+
w = imtohide.width
|
126 |
+
h = imtohide.height
|
127 |
+
if self.width*self.height*self.nbchannels < w*h*imtohide.channels:
|
128 |
+
raise SteganographyException("Carrier image not big enough to hold all the datas to steganography")
|
129 |
+
binw = self.binary_value(w, 16) #Width coded on to byte so width up to 65536
|
130 |
+
binh = self.binary_value(h, 16)
|
131 |
+
self.put_binary_value(binw) #Put width
|
132 |
+
self.put_binary_value(binh) #Put height
|
133 |
+
for h in range(imtohide.height): #Iterate the hole image to put every pixel values
|
134 |
+
for w in range(imtohide.width):
|
135 |
+
for chan in range(imtohide.channels):
|
136 |
+
val = imtohide[h,w][chan]
|
137 |
+
self.put_binary_value(self.byteValue(int(val)))
|
138 |
+
return self.image
|
139 |
+
|
140 |
+
|
141 |
+
def decode_image(self):
|
142 |
+
width = int(self.read_bits(16),2) #Read 16bits and convert it in int
|
143 |
+
height = int(self.read_bits(16),2)
|
144 |
+
unhideimg = np.zeros((width,height, 3), np.uint8) #Create an image in which we will put all the pixels read
|
145 |
+
for h in range(height):
|
146 |
+
for w in range(width):
|
147 |
+
for chan in range(unhideimg.channels):
|
148 |
+
val = list(unhideimg[h,w])
|
149 |
+
val[chan] = int(self.read_byte(),2) #Read the value
|
150 |
+
unhideimg[h,w] = tuple(val)
|
151 |
+
return unhideimg
|
152 |
+
|
153 |
+
def encode_binary(self, data):
|
154 |
+
l = len(data)
|
155 |
+
if self.width*self.height*self.nbchannels < l+64:
|
156 |
+
raise SteganographyException("Carrier image not big enough to hold all the datas to steganography")
|
157 |
+
self.put_binary_value(self.binary_value(l, 64))
|
158 |
+
for byte in data:
|
159 |
+
byte = byte if isinstance(byte, int) else ord(byte) # Compat py2/py3
|
160 |
+
self.put_binary_value(self.byteValue(byte))
|
161 |
+
return self.image
|
162 |
+
|
163 |
+
def decode_binary(self):
|
164 |
+
l = int(self.read_bits(64), 2)
|
165 |
+
output = b""
|
166 |
+
for i in range(l):
|
167 |
+
output += bytearray([int(self.read_byte(),2)])
|
168 |
+
return output
|
169 |
+
|
170 |
+
|
171 |
+
def main():
|
172 |
+
args = docopt.docopt(__doc__, version="0.2")
|
173 |
+
in_f = args["--in"]
|
174 |
+
out_f = args["--out"]
|
175 |
+
in_img = cv2.imread(in_f, cv2.IMREAD_UNCHANGED)
|
176 |
+
steg = LSBSteg(in_img)
|
177 |
+
lossy_formats = ["jpeg", "jpg"]
|
178 |
+
|
179 |
+
if args['encode']:
|
180 |
+
#Handling lossy format
|
181 |
+
out_f, out_ext = out_f.split(".")
|
182 |
+
if out_ext in lossy_formats:
|
183 |
+
out_f = out_f + ".png"
|
184 |
+
print("Output file changed to ", out_f)
|
185 |
+
|
186 |
+
data = open(args["--file"], "rb").read()
|
187 |
+
res = steg.encode_binary(data)
|
188 |
+
cv2.imwrite(out_f, res)
|
189 |
+
|
190 |
+
elif args["decode"]:
|
191 |
+
raw = steg.decode_binary()
|
192 |
+
with open(out_f, "wb") as f:
|
193 |
+
f.write(raw)
|
194 |
+
|
195 |
+
|
196 |
+
if __name__=="__main__":
|
197 |
+
main()
|
198 |
+
|
README.md
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Non-suspicious image decoder
|
3 |
+
emoji: 🥳
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: yellow
|
6 |
+
sdk: gradio
|
7 |
+
sdk_version: "3.13.2"
|
8 |
+
app_file: app.py
|
9 |
+
pinned: false
|
10 |
+
---
|
11 |
+
|
12 |
+
|
13 |
+
# Non Suspicious Image Decoder
|
14 |
+
Recover pandas dataframe hidden in image
|
__init__.py
ADDED
File without changes
|
app.py
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import cv2
|
3 |
+
import io
|
4 |
+
import pandas as pd
|
5 |
+
|
6 |
+
from LSBSteg import LSBSteg
|
7 |
+
|
8 |
+
|
9 |
+
def convert(file):
|
10 |
+
print(f"Converting file {file}")
|
11 |
+
in_img = cv2.imread(file, cv2.IMREAD_UNCHANGED)
|
12 |
+
lsbsteg = LSBSteg(in_img)
|
13 |
+
data = lsbsteg.decode_binary()
|
14 |
+
bytes = io.BytesIO(data)
|
15 |
+
dataframe = pd.read_parquet(bytes)
|
16 |
+
|
17 |
+
# dataframe.to_csv('output.csv')
|
18 |
+
return dataframe
|
19 |
+
|
20 |
+
|
21 |
+
with gr.Blocks() as demo:
|
22 |
+
gr.Markdown("""
|
23 |
+
## Non-Suspicious image decoder
|
24 |
+
|
25 |
+
This tool shows the extraction a dataframe hidden inside an image.
|
26 |
+
|
27 |
+
There are a few ways to hide data into a PNG file, notably:
|
28 |
+
* adding it after the end of the file (after the PNG IEND chunk), so that it gets
|
29 |
+
ignored by image viewers
|
30 |
+
* adding it as comments in the PNG file (tEXt chunks)
|
31 |
+
|
32 |
+
These methods are kind of easy to spot! Also, a lot of software, browsers, image upload
|
33 |
+
websites etc often just strip them.
|
34 |
+
|
35 |
+
So, here, we have a different, more thoughtful (and arguably cooler) method.
|
36 |
+
|
37 |
+
This class hides the data using a basic kind of **[steganography](https://en.wikipedia.org/wiki/Steganography)**:
|
38 |
+
it hides it in the
|
39 |
+
*least significant bits* of the raw (uncompressed) picture: tiny differences in the red, green and blue
|
40 |
+
channel of the image encodes the data we're interested in.
|
41 |
+
|
42 |
+
This means the resulting picture
|
43 |
+
looks **very close to the original image**; and for the data we hide here, it is **inperceptible
|
44 |
+
to the naked eye**.
|
45 |
+
|
46 |
+
The resulting PNG file will probably get a little bit bigger as a result, since PNG uses compression,
|
47 |
+
which will have a harder time when we have our stolen data injected into the image. This is
|
48 |
+
not that much of a problem since it stays <100Ko, so it's not that noticeable.
|
49 |
+
|
50 |
+
""")
|
51 |
+
with gr.Row():
|
52 |
+
im = gr.Image(label="Input image file", type="filepath")
|
53 |
+
|
54 |
+
def preprocess(encoding: str) -> str:
|
55 |
+
# We do our own preprocessing because gradio's deletes PNG metadata :(
|
56 |
+
import tempfile
|
57 |
+
import base64
|
58 |
+
|
59 |
+
content = encoding.split(";")[1]
|
60 |
+
image_encoded = content.split(",")[1]
|
61 |
+
png_content = base64.b64decode(image_encoded)
|
62 |
+
file_obj = tempfile.NamedTemporaryFile(
|
63 |
+
delete=False,
|
64 |
+
suffix=".input.png",
|
65 |
+
)
|
66 |
+
file_obj.write(png_content)
|
67 |
+
return file_obj.name
|
68 |
+
|
69 |
+
im.preprocess = preprocess
|
70 |
+
df_out = gr.Dataframe(
|
71 |
+
label="Output dataframe", max_rows=20, overflow_row_behaviour="paginate"
|
72 |
+
)
|
73 |
+
# file_out = gr.File(label="Full output CSV file")
|
74 |
+
btn = gr.Button(value="Extract")
|
75 |
+
gr.Markdown("Click on the example below to get the data from the associated colab notebook :)")
|
76 |
+
gr.Examples(
|
77 |
+
examples=["sample-picture.png"],
|
78 |
+
inputs=[im],
|
79 |
+
outputs=[df_out],
|
80 |
+
fn=convert,
|
81 |
+
cache_examples=True,
|
82 |
+
)
|
83 |
+
# demo = gr.Interface(convert, im, im_2)
|
84 |
+
btn.click(convert, inputs=[im], outputs=[df_out])
|
85 |
+
|
86 |
+
# example_img = os.path.join(os.path.dirname(__file__), "example-picture.png")
|
87 |
+
|
88 |
+
if __name__ == "__main__":
|
89 |
+
demo.launch()
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
opencv-python
|
2 |
+
docopt
|
3 |
+
numpy
|
sample-picture.png
ADDED
![]() |
Git LFS Details
|