shadowbits 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shadowbits/cli.py +74 -0
- shadowbits/crypto/aes.py +28 -0
- shadowbits/emb_aud.py +99 -0
- shadowbits/emb_img.py +103 -0
- shadowbits/ext_aud.py +74 -0
- shadowbits/ext_img.py +91 -0
- shadowbits/validator.py +87 -0
- shadowbits-1.0.0.dist-info/METADATA +207 -0
- shadowbits-1.0.0.dist-info/RECORD +13 -0
- shadowbits-1.0.0.dist-info/WHEEL +5 -0
- shadowbits-1.0.0.dist-info/entry_points.txt +2 -0
- shadowbits-1.0.0.dist-info/licenses/LICENSE +21 -0
- shadowbits-1.0.0.dist-info/top_level.txt +1 -0
shadowbits/cli.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
import argparse
|
2
|
+
from emb_img import embed_file
|
3
|
+
from ext_img import extract_file
|
4
|
+
from emb_aud import embed_audio
|
5
|
+
from ext_aud import extract_audio
|
6
|
+
|
7
|
+
|
8
|
+
def main():
|
9
|
+
parser = argparse.ArgumentParser(description='Steganography tool for hiding files in images')
|
10
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
11
|
+
|
12
|
+
|
13
|
+
# Embed command for image
|
14
|
+
img_parser = subparsers.add_parser('img', help='Image operations')
|
15
|
+
img_subparser = img_parser.add_subparsers(dest='action' , help='Image actions')
|
16
|
+
embed_cmd = img_subparser.add_parser('embed', help='Hide a file in an image')
|
17
|
+
embed_cmd.add_argument('--in', dest='input', required=True, help='File to hide')
|
18
|
+
embed_cmd.add_argument('--cover', dest='image', required=True, help='Cover image')
|
19
|
+
embed_cmd.add_argument('--key', required=True, help='Secret key for randomization')
|
20
|
+
|
21
|
+
# Extract command for image
|
22
|
+
extract_cmd = img_subparser.add_parser('extract', help='Extract hidden file from image')
|
23
|
+
extract_cmd.add_argument('--stego', required=True, help='Image with hidden file')
|
24
|
+
extract_cmd.add_argument('--key', required=True, help='Secret key used for hiding')
|
25
|
+
|
26
|
+
# Embed command for audio
|
27
|
+
aud_parser = subparsers.add_parser('aud', help='Audio operation')
|
28
|
+
aud_subparser = aud_parser.add_subparsers(dest='action', help='Audio actions')
|
29
|
+
embed_audio_cmd = aud_subparser.add_parser('embed', help='Hide a file in an audio file')
|
30
|
+
embed_audio_cmd.add_argument('--in', dest='input', required=True, help='File to hide')
|
31
|
+
embed_audio_cmd.add_argument('--cover', dest='song', required=True, help='Cover audio')
|
32
|
+
embed_audio_cmd.add_argument('--key', required=True, help='Secret key to randomize bits')
|
33
|
+
|
34
|
+
# Extract command for audio
|
35
|
+
extract_audio_cmd = aud_subparser.add_parser('extract', help='Extract hidden file from audio')
|
36
|
+
extract_audio_cmd.add_argument('--stego', required=True, help='Audio with hidden file')
|
37
|
+
extract_audio_cmd.add_argument('--key', required=True, help='Secret key used for hiding')
|
38
|
+
|
39
|
+
|
40
|
+
args = parser.parse_args()
|
41
|
+
|
42
|
+
if not args.command:
|
43
|
+
parser.print_help()
|
44
|
+
exit(1)
|
45
|
+
|
46
|
+
try:
|
47
|
+
if args.command == 'img':
|
48
|
+
if args.action == 'embed':
|
49
|
+
embed_file(args.image, args.input, args.key)
|
50
|
+
print(f"Successfully embedded {args.input} in {args.image}.")
|
51
|
+
elif args.action == 'extract':
|
52
|
+
extract_file(args.stego, args.key)
|
53
|
+
print(f"Successfully extracted hidden file from {args.stego}.")
|
54
|
+
else:
|
55
|
+
img_parser.print_help()
|
56
|
+
raise SystemExit(1)
|
57
|
+
|
58
|
+
elif args.command == 'aud':
|
59
|
+
if args.action == 'embed':
|
60
|
+
embed_audio(args.song, args.input, args.key)
|
61
|
+
print(f"Successfully embedded {args.input} in {args.song}.")
|
62
|
+
elif args.action == 'extract':
|
63
|
+
extract_audio(args.stego, args.key)
|
64
|
+
print(f"Successfully extracted hidden file from {args.stego}.")
|
65
|
+
else:
|
66
|
+
aud_parser.print_help()
|
67
|
+
raise SystemExit(1)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
print(f"Error: {e}")
|
71
|
+
exit(1)
|
72
|
+
|
73
|
+
if __name__ == '__main__':
|
74
|
+
main()
|
shadowbits/crypto/aes.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import hashlib
|
2
|
+
from Crypto.Cipher import AES
|
3
|
+
|
4
|
+
def derive_key(password):
|
5
|
+
return hashlib.sha256(password.encode()).digest()
|
6
|
+
|
7
|
+
def to_seed(password: str) -> int:
|
8
|
+
digest = hashlib.sha256(password.encode()).digest()
|
9
|
+
return int.from_bytes(digest, 'big')
|
10
|
+
|
11
|
+
def encryption(payload, key):
|
12
|
+
cipher = AES.new(derive_key(key), AES.MODE_EAX)
|
13
|
+
ciphertext, tag = cipher.encrypt_and_digest(payload)
|
14
|
+
payload = cipher.nonce + tag + ciphertext
|
15
|
+
return payload
|
16
|
+
|
17
|
+
def decryption(payload, key):
|
18
|
+
if len(payload) < 32:
|
19
|
+
raise ValueError("Encrypted payload too short")
|
20
|
+
nonce = payload[:16]
|
21
|
+
tag = payload[16:32]
|
22
|
+
ciphertext = payload[32:]
|
23
|
+
cipher = AES.new(derive_key(key), AES.MODE_EAX, nonce=nonce)
|
24
|
+
try:
|
25
|
+
payload = cipher.decrypt_and_verify(ciphertext, tag)
|
26
|
+
return payload
|
27
|
+
except ValueError as e:
|
28
|
+
raise ValueError("Decryption failed - wrong key or corrupted data")
|
shadowbits/emb_aud.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
import wave
|
2
|
+
import random
|
3
|
+
from crypto.aes import *
|
4
|
+
import os
|
5
|
+
import time
|
6
|
+
|
7
|
+
def valid_wav(cover_path):
|
8
|
+
try:
|
9
|
+
with wave.open(cover_path, 'rb') as wave_file:
|
10
|
+
channels = wave_file.getnchannels()
|
11
|
+
sample_width = wave_file.getsampwidth()
|
12
|
+
framerate = wave_file.getframerate()
|
13
|
+
frames = wave_file.getnframes()
|
14
|
+
|
15
|
+
if not (1 <= channels <= 8):
|
16
|
+
return False
|
17
|
+
if sample_width not in [1, 2, 3, 4]:
|
18
|
+
return False
|
19
|
+
if not (8000 <= framerate <= 192000):
|
20
|
+
return False
|
21
|
+
if frames <= 0:
|
22
|
+
return False
|
23
|
+
|
24
|
+
wave_file.readframes(min(1024, frames))
|
25
|
+
|
26
|
+
return True
|
27
|
+
|
28
|
+
except (wave.Error, EOFError, FileNotFoundError, PermissionError):
|
29
|
+
return False
|
30
|
+
except Exception:
|
31
|
+
return False
|
32
|
+
|
33
|
+
|
34
|
+
def embed_audio(cover_path, payload_path, key):
|
35
|
+
start = time.time()
|
36
|
+
try:
|
37
|
+
if not os.path.exists(cover_path):
|
38
|
+
raise FileNotFoundError(f"Cover file path {cover_path} not found.")
|
39
|
+
if not os.path.exists(payload_path):
|
40
|
+
raise FileNotFoundError(f"Secret file path {payload_path} not found.")
|
41
|
+
if not valid_wav(cover_path):
|
42
|
+
raise ValueError(f"{cover_path} is not a valid WAV file")
|
43
|
+
|
44
|
+
print("Loading cover file...")
|
45
|
+
with wave.open(cover_path, mode='rb') as song:
|
46
|
+
frame_bytes = bytearray(song.readframes(song.getnframes()))
|
47
|
+
params = song.getparams()
|
48
|
+
|
49
|
+
print("Loading payload file...")
|
50
|
+
with open(payload_path, 'rb') as f:
|
51
|
+
payload = f.read()
|
52
|
+
|
53
|
+
print("Encrypting the payload's byte...")
|
54
|
+
payload = encryption(payload, key)
|
55
|
+
|
56
|
+
print("Attaching the markers on payload's bytes...")
|
57
|
+
starting = b'###START###'
|
58
|
+
ending = b'###END###'
|
59
|
+
full_payload = starting + payload + ending
|
60
|
+
|
61
|
+
print("Converting the payload's byte into bits...")
|
62
|
+
payload_bits = []
|
63
|
+
for byte in full_payload:
|
64
|
+
bits = format(byte, '08b')
|
65
|
+
payload_bits.extend([int(b) for b in bits])
|
66
|
+
|
67
|
+
print("Checking cover file capacity...")
|
68
|
+
max_payload_bits = len(frame_bytes)
|
69
|
+
if len(payload_bits) > max_payload_bits:
|
70
|
+
raise ValueError(f"Payload too large! Need {len(payload_bits)} bits but only have {max_payload_bits} available")
|
71
|
+
|
72
|
+
seed = to_seed(key)
|
73
|
+
shifu = random.Random(seed)
|
74
|
+
indexes = list(range(len(frame_bytes)))
|
75
|
+
shifu.shuffle(indexes)
|
76
|
+
|
77
|
+
print("Embedding the payload bits into cover file bytes...")
|
78
|
+
for bit_idx, bit in enumerate(payload_bits):
|
79
|
+
i = indexes[bit_idx]
|
80
|
+
frame_bytes[i] = (frame_bytes[i] & 0xFE) | bit
|
81
|
+
|
82
|
+
output_path = "encoded.wav"
|
83
|
+
if os.path.exists(output_path):
|
84
|
+
counter = 1
|
85
|
+
while os.path.exists(f"encoded({counter}).wav"):
|
86
|
+
counter += 1
|
87
|
+
output_path = f"encoded({counter}).wav"
|
88
|
+
|
89
|
+
print("Saving stego file...")
|
90
|
+
with wave.open(output_path, 'wb') as fd:
|
91
|
+
fd.setparams(params)
|
92
|
+
fd.writeframes(bytes(frame_bytes))
|
93
|
+
end = time.time() - start
|
94
|
+
print(f"Time taken: {int(end)} seconds.")
|
95
|
+
return output_path
|
96
|
+
|
97
|
+
except Exception as e:
|
98
|
+
print(f"Error in embed_audio: {e}")
|
99
|
+
raise
|
shadowbits/emb_img.py
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
from PIL import Image
|
2
|
+
import random
|
3
|
+
from crypto.aes import *
|
4
|
+
import os
|
5
|
+
import time
|
6
|
+
|
7
|
+
### To varify if the image is valid
|
8
|
+
def valid_img(cover_path):
|
9
|
+
try:
|
10
|
+
with Image.open(cover_path) as img:
|
11
|
+
# Check format first
|
12
|
+
if img.format and img.format.upper() == "PNG":
|
13
|
+
img.verify() # Only verify if it claims to be PNG
|
14
|
+
return True
|
15
|
+
return False
|
16
|
+
except Exception:
|
17
|
+
return False
|
18
|
+
|
19
|
+
|
20
|
+
def embed_file(cover_path, payload_path, key):
|
21
|
+
start = time.time()
|
22
|
+
try:
|
23
|
+
if not os.path.exists(cover_path):
|
24
|
+
raise FileNotFoundError(f"Cover image {cover_path} not found.")
|
25
|
+
if not os.path.exists(payload_path):
|
26
|
+
raise FileNotFoundError(f"Payload file {payload_path} not found.")
|
27
|
+
if not valid_img(cover_path):
|
28
|
+
raise ValueError(f"{cover_path} is not a valid PNG image file.")
|
29
|
+
|
30
|
+
print("Loading cover file...")
|
31
|
+
img = Image.open(cover_path)
|
32
|
+
mode = img.mode
|
33
|
+
|
34
|
+
if mode != 'RGB':
|
35
|
+
print(f"Converting the {mode} mode to RGB mode...")
|
36
|
+
img = img.convert('RGB')
|
37
|
+
|
38
|
+
pixels = list(img.getdata())
|
39
|
+
|
40
|
+
print("Loading payload file in bytes...")
|
41
|
+
with open(payload_path, 'rb') as f:
|
42
|
+
payload = f.read()
|
43
|
+
|
44
|
+
print("Encrypting the payload bytes...")
|
45
|
+
payload = encryption(payload, key)
|
46
|
+
|
47
|
+
print("Attaching the markers on payload bytes...")
|
48
|
+
starting = b'###START###'
|
49
|
+
ending = b'###END###'
|
50
|
+
|
51
|
+
length = len(payload).to_bytes(4, 'big')
|
52
|
+
data = starting + length + payload + ending
|
53
|
+
|
54
|
+
print("Convertng payload bytes into bits...")
|
55
|
+
bits = ''.join(f'{byte:08b}' for byte in data)
|
56
|
+
|
57
|
+
max_bits = len(pixels) * 3 ### 3 color channels per pixel
|
58
|
+
print("Checking cover file capacity...")
|
59
|
+
if len(bits) > max_bits:
|
60
|
+
raise ValueError('Payload is too large to embed in cover file.')
|
61
|
+
|
62
|
+
seed = to_seed(key)
|
63
|
+
prng = random.Random(seed)
|
64
|
+
indexes = list(range(len(pixels)))
|
65
|
+
prng.shuffle(indexes)
|
66
|
+
|
67
|
+
print("Embedding data into image...")
|
68
|
+
new_pixels = [None] * len(pixels)
|
69
|
+
bit_idx = 0
|
70
|
+
|
71
|
+
for i in range(len(indexes)):
|
72
|
+
pixel_idx = indexes[i]
|
73
|
+
r, g, b = pixels[pixel_idx]
|
74
|
+
|
75
|
+
if bit_idx < len(bits):
|
76
|
+
r = (r & ~1) | int(bits[bit_idx])
|
77
|
+
bit_idx += 1
|
78
|
+
if bit_idx < len(bits):
|
79
|
+
g = (g & ~1) | int(bits[bit_idx])
|
80
|
+
bit_idx += 1
|
81
|
+
if bit_idx < len(bits):
|
82
|
+
b = (b & ~1) | int(bits[bit_idx])
|
83
|
+
bit_idx += 1
|
84
|
+
|
85
|
+
new_pixels[pixel_idx] = (r, g, b)
|
86
|
+
|
87
|
+
img.putdata(new_pixels)
|
88
|
+
|
89
|
+
output_path = "stego_file.png"
|
90
|
+
if os.path.exists(output_path):
|
91
|
+
counter = 1
|
92
|
+
while os.path.exists(f"stego_file({counter}).png"):
|
93
|
+
counter += 1
|
94
|
+
output_path = f"stego_file({counter}).png"
|
95
|
+
|
96
|
+
print("Saving the stego file...")
|
97
|
+
file = img.save(output_path)
|
98
|
+
end = time.time() - start
|
99
|
+
print(f"Time taken: {int(end)} seconds.")
|
100
|
+
return file
|
101
|
+
except Exception as e:
|
102
|
+
print(f"Error in embedding : {e}")
|
103
|
+
raise
|
shadowbits/ext_aud.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
import wave
|
2
|
+
from emb_aud import valid_wav
|
3
|
+
from crypto.aes import *
|
4
|
+
import os
|
5
|
+
import random
|
6
|
+
from validator import detect_file_type
|
7
|
+
import time
|
8
|
+
|
9
|
+
def extract_audio(stego_path, key):
|
10
|
+
start = time.time()
|
11
|
+
try:
|
12
|
+
if not valid_wav(stego_path):
|
13
|
+
raise ValueError(f"{stego_path} is not a valid wav file")
|
14
|
+
|
15
|
+
print("Loading stego file...")
|
16
|
+
with wave.open(stego_path, 'rb') as song:
|
17
|
+
frame_bytes = song.readframes(song.getnframes())
|
18
|
+
|
19
|
+
seed = to_seed(key)
|
20
|
+
shifu = random.Random(seed)
|
21
|
+
indexes = list(range(len(frame_bytes)))
|
22
|
+
shifu.shuffle(indexes)
|
23
|
+
|
24
|
+
print("Extracting the bits from stego file...")
|
25
|
+
extracted_bits = []
|
26
|
+
for i in indexes:
|
27
|
+
extracted_bits.append(frame_bytes[i] & 1)
|
28
|
+
|
29
|
+
print("Converting extracted bits into bytes...")
|
30
|
+
extracted_bytes = bytearray()
|
31
|
+
for i in range(0, len(extracted_bits), 8):
|
32
|
+
if i + 7 < len(extracted_bits):
|
33
|
+
byte_bits = extracted_bits[i:i+8]
|
34
|
+
byte_value = 0
|
35
|
+
for j, bit in enumerate(byte_bits):
|
36
|
+
byte_value |= bit << (7-j)
|
37
|
+
extracted_bytes.append(byte_value)
|
38
|
+
|
39
|
+
starting = b'###START###'
|
40
|
+
ending = b'###END###'
|
41
|
+
print("Locating markers in bytes...")
|
42
|
+
try:
|
43
|
+
start_byte = extracted_bytes.find(starting)
|
44
|
+
if start_byte == -1:
|
45
|
+
raise ValueError("Starting point not found - file may not contain embedded data or key is incorrect")
|
46
|
+
|
47
|
+
search = start_byte + len(starting)
|
48
|
+
end_byte = extracted_bytes.find(ending, search)
|
49
|
+
if end_byte == -1:
|
50
|
+
raise ValueError("Ending point not found - embedded data maybe corrupted")
|
51
|
+
|
52
|
+
print("Extracting payload's bytes from bytes....")
|
53
|
+
payload = bytes(extracted_bytes[search:end_byte])
|
54
|
+
|
55
|
+
except Exception as e:
|
56
|
+
print(f"Error finding payload data : {e}")
|
57
|
+
return None
|
58
|
+
|
59
|
+
print("Decrypting the payload's bytes...")
|
60
|
+
payload = decryption(payload, key)
|
61
|
+
|
62
|
+
extension, mime_type = detect_file_type(payload)
|
63
|
+
output_path = f"hidden_file.{extension}"
|
64
|
+
|
65
|
+
print("Saving the extracted file...")
|
66
|
+
with open(output_path, 'wb') as fd:
|
67
|
+
fd.write(payload)
|
68
|
+
end = time.time() - start
|
69
|
+
print(f"Time taken: {int(end)} seconds.")
|
70
|
+
return payload
|
71
|
+
|
72
|
+
except Exception as e:
|
73
|
+
print(f"Error in extracting data from file: {e}")
|
74
|
+
return None
|
shadowbits/ext_img.py
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
from PIL import Image
|
2
|
+
import random
|
3
|
+
from crypto.aes import *
|
4
|
+
import os
|
5
|
+
from emb_img import valid_img
|
6
|
+
from validator import detect_file_type
|
7
|
+
import time
|
8
|
+
|
9
|
+
def extract_file(stego_path, key):
|
10
|
+
start = time.time()
|
11
|
+
try:
|
12
|
+
if not os.path.exists(stego_path):
|
13
|
+
raise FileNotFoundError(f"stego image {stego_path} not found.")
|
14
|
+
if not valid_img(stego_path):
|
15
|
+
raise ValueError(f"Image is not a valid PNG file.")
|
16
|
+
|
17
|
+
print("Loading stego file...")
|
18
|
+
img = Image.open(stego_path)
|
19
|
+
pixels = list(img.getdata())
|
20
|
+
|
21
|
+
seed = to_seed(key)
|
22
|
+
prng = random.Random(seed)
|
23
|
+
indexes = list(range(len(pixels)))
|
24
|
+
prng.shuffle(indexes)
|
25
|
+
|
26
|
+
print("Extracting the bits from stego file...")
|
27
|
+
bits = ''
|
28
|
+
for i in range(len(pixels)):
|
29
|
+
pixel_idx = indexes[i]
|
30
|
+
r, g, b = pixels[pixel_idx]
|
31
|
+
bits += str(r & 1)
|
32
|
+
bits += str(g & 1)
|
33
|
+
bits += str(b & 1)
|
34
|
+
|
35
|
+
|
36
|
+
if len(bits) % 8 != 0:
|
37
|
+
bits = bits[:-(len(bits) % 8)] # Remove incomplete byte
|
38
|
+
|
39
|
+
print("Converting bits into bytes...")
|
40
|
+
bytes_list = bytes([int(bits[i:i+8], 2) for i in range(0, len(bits), 8)])
|
41
|
+
|
42
|
+
starting = b'###START###'
|
43
|
+
ending = b'###END###'
|
44
|
+
|
45
|
+
print("Locating markers in bytes...")
|
46
|
+
start_point = bytes_list.find(starting)
|
47
|
+
if start_point == -1:
|
48
|
+
raise ValueError("Starting marker of the payload not found in image or key is incorrect")
|
49
|
+
|
50
|
+
data_start = start_point + len(starting)
|
51
|
+
|
52
|
+
if len(bytes_list) < data_start + 4:
|
53
|
+
raise ValueError("Not enough data to extract length")
|
54
|
+
|
55
|
+
length = int.from_bytes(bytes_list[data_start:data_start + 4], 'big')
|
56
|
+
|
57
|
+
payload_start = data_start + 4
|
58
|
+
payload_end = payload_start + length
|
59
|
+
|
60
|
+
if len(bytes_list) < payload_end:
|
61
|
+
raise ValueError(f"Not enough data to extract payload of length {length}")
|
62
|
+
|
63
|
+
print("Extracting payload's bytes from bytes...")
|
64
|
+
payload = bytes_list[payload_start:payload_end]
|
65
|
+
|
66
|
+
print("Verifying the end marker in bytes...")
|
67
|
+
end_start = payload_end
|
68
|
+
end_end = end_start + len(ending)
|
69
|
+
|
70
|
+
if len(bytes_list) < end_end:
|
71
|
+
raise ValueError(f"Not enough data to verify the end marker.")
|
72
|
+
|
73
|
+
extract_end = bytes_list[end_start:end_end]
|
74
|
+
if extract_end != ending:
|
75
|
+
raise ValueError(f"End marker not found or corrupted")
|
76
|
+
|
77
|
+
print("Decrypting the payload bytes...")
|
78
|
+
payload = decryption(payload, key)
|
79
|
+
|
80
|
+
extension, mime_type = detect_file_type(payload)
|
81
|
+
out_path = f"extracted_file.{extension}"
|
82
|
+
|
83
|
+
print("Saving the extracted file...")
|
84
|
+
with open(out_path, 'wb') as f:
|
85
|
+
f.write(payload)
|
86
|
+
end = time.time() - start
|
87
|
+
print(f"Time taken: {int(end)} seconds.")
|
88
|
+
return out_path
|
89
|
+
except Exception as e:
|
90
|
+
print(f"Error in extracion : {e}")
|
91
|
+
raise
|
shadowbits/validator.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
def detect_file_type(data):
|
2
|
+
"""
|
3
|
+
Detect file type based on magic bytes (file signatures)
|
4
|
+
Returns tuple: (extension, mime_type)
|
5
|
+
"""
|
6
|
+
if not data or len(data) < 4:
|
7
|
+
return 'bin', 'application/octet-stream'
|
8
|
+
|
9
|
+
# Common file signatures
|
10
|
+
signatures = {
|
11
|
+
# Images
|
12
|
+
b'\xFF\xD8\xFF': ('jpg', 'image/jpeg'),
|
13
|
+
b'\x89PNG\r\n\x1a\n': ('png', 'image/png'),
|
14
|
+
b'GIF87a': ('gif', 'image/gif'),
|
15
|
+
b'GIF89a': ('gif', 'image/gif'),
|
16
|
+
b'BM': ('bmp', 'image/bmp'),
|
17
|
+
b'RIFF': ('webp', 'image/webp'),
|
18
|
+
b'\x00\x00\x01\x00': ('ico', 'image/x-icon'),
|
19
|
+
|
20
|
+
# Documents
|
21
|
+
b'%PDF': ('pdf', 'application/pdf'),
|
22
|
+
b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1': ('doc', 'application/msword'),
|
23
|
+
b'PK\x03\x04': ('zip', 'application/zip'), # Also docx, xlsx, etc.
|
24
|
+
|
25
|
+
# Audio
|
26
|
+
b'ID3': ('mp3', 'audio/mpeg'),
|
27
|
+
b'\xFF\xFB': ('mp3', 'audio/mpeg'),
|
28
|
+
b'\xFF\xF3': ('mp3', 'audio/mpeg'),
|
29
|
+
b'\xFF\xF2': ('mp3', 'audio/mpeg'),
|
30
|
+
b'OggS': ('ogg', 'audio/ogg'),
|
31
|
+
b'fLaC': ('flac', 'audio/flac'),
|
32
|
+
|
33
|
+
# Video
|
34
|
+
b'\x00\x00\x00\x18ftypmp4': ('mp4', 'video/mp4'),
|
35
|
+
b'\x00\x00\x00\x20ftypM4V': ('m4v', 'video/x-m4v'),
|
36
|
+
b'RIFF....AVI ': ('avi', 'video/x-msvideo'),
|
37
|
+
|
38
|
+
# Archives
|
39
|
+
b'\x50\x4B\x05\x06': ('zip', 'application/zip'),
|
40
|
+
b'\x50\x4B\x07\x08': ('zip', 'application/zip'),
|
41
|
+
b'\x1F\x8B\x08': ('gz', 'application/gzip'),
|
42
|
+
b'Rar!\x1A\x07\x00': ('rar', 'application/x-rar-compressed'),
|
43
|
+
b'7z\xBC\xAF\x27\x1C': ('7z', 'application/x-7z-compressed'),
|
44
|
+
|
45
|
+
# Text/Code
|
46
|
+
b'#!/bin/bash': ('sh', 'text/x-shellscript'),
|
47
|
+
b'#!/bin/sh': ('sh', 'text/x-shellscript'),
|
48
|
+
b'<?xml': ('xml', 'text/xml'),
|
49
|
+
b'<html': ('html', 'text/html'),
|
50
|
+
b'<!DOCTYPE html': ('html', 'text/html'),
|
51
|
+
}
|
52
|
+
|
53
|
+
for sig, (ext, mime) in signatures.items():
|
54
|
+
if data.startswith(sig):
|
55
|
+
# Special handling for RIFF files
|
56
|
+
if sig == b'RIFF' and len(data) >= 12:
|
57
|
+
if data[8:12] == b'WAVE':
|
58
|
+
return 'wav', 'audio/wav'
|
59
|
+
elif data[8:12] == b'WEBP':
|
60
|
+
return 'webp', 'image/webp'
|
61
|
+
elif data[8:12] == b'AVI ':
|
62
|
+
return 'avi', 'video/x-msvideo'
|
63
|
+
return ext, mime
|
64
|
+
|
65
|
+
# Check for text files
|
66
|
+
try:
|
67
|
+
text = data[:min(512, len(data))].decode('utf-8', errors='strict')
|
68
|
+
if all(ord(c) < 127 and (c.isprintable() or c.isspace()) for c in text):
|
69
|
+
# Further classify text files
|
70
|
+
text_lower = text.lower()
|
71
|
+
if any(keyword in text_lower for keyword in ['<html', '<!doctype', '<head', '<body']):
|
72
|
+
return 'html', 'text/html'
|
73
|
+
elif text.strip().startswith('<?xml'):
|
74
|
+
return 'xml', 'text/xml'
|
75
|
+
elif any(keyword in text_lower for keyword in ['import ', 'def ', 'class ', 'if __name__']):
|
76
|
+
return 'py', 'text/x-python'
|
77
|
+
elif any(keyword in text_lower for keyword in ['#include', 'int main', 'void ']):
|
78
|
+
return 'c', 'text/x-c'
|
79
|
+
elif any(keyword in text_lower for keyword in ['function', 'var ', 'let ', 'const ']):
|
80
|
+
return 'js', 'text/javascript'
|
81
|
+
else:
|
82
|
+
return 'txt', 'text/plain'
|
83
|
+
except UnicodeDecodeError:
|
84
|
+
pass
|
85
|
+
|
86
|
+
# If no signature matches, return binary
|
87
|
+
return 'bin', 'application/octet-stream'
|
@@ -0,0 +1,207 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: shadowbits
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Powerful steganography tool for hiding files in images and audio using LSB techniques
|
5
|
+
Author-email: kaizoku73 <ultimate0134@gmail.com>
|
6
|
+
Maintainer-email: kaizoku73 <ultimate0134@gmail.com>
|
7
|
+
License: MIT
|
8
|
+
Project-URL: Homepage, https://github.com/kaizoku73/ShadowBits
|
9
|
+
Project-URL: Repository, https://github.com/kaizoku73/ShadowBits
|
10
|
+
Project-URL: Bug Reports, https://github.com/kaizoku73/ShadowBits/issues
|
11
|
+
Project-URL: Documentation, https://kaizoku.gitbook.io/steganography
|
12
|
+
Project-URL: LSB Image Docs, https://kaizoku.gitbook.io/steganography/lsb-for-image
|
13
|
+
Project-URL: LSB Audio Docs, https://kaizoku.gitbook.io/steganography/lsb-for-audio
|
14
|
+
Keywords: steganography,lsb,least-significant-bit,image-steganography,audio-steganography,cryptography,security,png,wav,file-hiding,aes-encryption,bits-manipulation
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
16
|
+
Classifier: Intended Audience :: Developers
|
17
|
+
Classifier: Intended Audience :: End Users/Desktop
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
19
|
+
Classifier: Topic :: Multimedia :: Graphics
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
22
|
+
Classifier: License :: OSI Approved :: MIT License
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
24
|
+
Classifier: Programming Language :: Python :: 3.7
|
25
|
+
Classifier: Programming Language :: Python :: 3.8
|
26
|
+
Classifier: Programming Language :: Python :: 3.9
|
27
|
+
Classifier: Programming Language :: Python :: 3.10
|
28
|
+
Classifier: Programming Language :: Python :: 3.11
|
29
|
+
Classifier: Programming Language :: Python :: 3.12
|
30
|
+
Classifier: Operating System :: OS Independent
|
31
|
+
Requires-Python: >=3.7
|
32
|
+
Description-Content-Type: text/markdown
|
33
|
+
License-File: LICENSE
|
34
|
+
Requires-Dist: future>=0.18.0
|
35
|
+
Requires-Dist: Pillow>=8.0.0
|
36
|
+
Requires-Dist: pycryptodome>=3.15.0
|
37
|
+
Dynamic: license-file
|
38
|
+
|
39
|
+
# ShadowBits 🔒
|
40
|
+
|
41
|
+
A powerful steganography tool that allows you to hide files within images and audio files using LSB (Least Significant Bit) techniques. ShadowBits supports both embedding and extraction operations with optional AES encryption for enhanced security.
|
42
|
+
|
43
|
+
## Features
|
44
|
+
|
45
|
+
- **Image Steganography**: Hide files within PNG images using LSB manipulation
|
46
|
+
- **Audio Steganography**: Embed files in WAV audio files
|
47
|
+
- **AES Encryption**: Optional encryption layer for embedded data
|
48
|
+
- **Key-based Randomization**: Uses secret keys to randomize bit placement for enhanced security
|
49
|
+
- **Automatic File Type Detection**: Detects and preserves original file types during extraction
|
50
|
+
- **Format Validation**: Validates PNG images and WAV audio files before processing
|
51
|
+
- **Collision Prevention**: Automatically handles filename conflicts during output
|
52
|
+
- **Comprehensive Error Handling**: Robust error handling for various failure scenarios
|
53
|
+
|
54
|
+
## Quick Start
|
55
|
+
|
56
|
+
### Prerequisites
|
57
|
+
|
58
|
+
```bash
|
59
|
+
pip install -r requirements.txt
|
60
|
+
```
|
61
|
+
### Installation
|
62
|
+
|
63
|
+
```bash
|
64
|
+
pip install shadowbits
|
65
|
+
```
|
66
|
+
|
67
|
+
### One-Line Installation
|
68
|
+
|
69
|
+
```bash
|
70
|
+
# System-wide (with sudo)
|
71
|
+
curl -sSL https://raw.githubusercontent.com/kaizoku73/ShadowBits/main/install.sh | sudo bash
|
72
|
+
|
73
|
+
# User installation (no sudo)
|
74
|
+
curl -sSL https://raw.githubusercontent.com/kaizoku73/ShadowBits/main/install.sh | bash
|
75
|
+
```
|
76
|
+
|
77
|
+
### Uninstallation
|
78
|
+
|
79
|
+
```bash
|
80
|
+
# To uninstall Resono (no sudo):
|
81
|
+
curl -sSL https://raw.githubusercontent.com/kaizoku73/ShadowBits/main/uninstall.sh | bash
|
82
|
+
|
83
|
+
# To uninstall Resono (With sudo):
|
84
|
+
curl -sSL https://raw.githubusercontent.com/kaizoku73/ShadowBits/main/uninstall.sh | sudo bash
|
85
|
+
|
86
|
+
```
|
87
|
+
|
88
|
+
## Usage
|
89
|
+
|
90
|
+
ShadowBits provides a command-line interface with four main operations:
|
91
|
+
|
92
|
+
### Image Operations
|
93
|
+
|
94
|
+
#### Embed a file in an image
|
95
|
+
```bash
|
96
|
+
shadowbits img embed --in secret.txt --cover image.png --key mysecretkey
|
97
|
+
```
|
98
|
+
|
99
|
+
#### Extract from image
|
100
|
+
```bash
|
101
|
+
shadowbits img extract --stego stego_image.png --key mysecretkey
|
102
|
+
```
|
103
|
+
|
104
|
+
|
105
|
+
### Audio Operations
|
106
|
+
|
107
|
+
#### Embed a file in audio
|
108
|
+
```bash
|
109
|
+
shadowbits aud embed --in secret.pdf --cover music.wav --key myaudiokey
|
110
|
+
```
|
111
|
+
|
112
|
+
#### Extract from audio
|
113
|
+
```bash
|
114
|
+
shadowbits aud extract --stego stego_audio.wav --key myaudiokey
|
115
|
+
```
|
116
|
+
|
117
|
+
## How It Works
|
118
|
+
|
119
|
+
### LSB Steganography
|
120
|
+
ShadowBits uses the Least Significant Bit (LSB) method to hide data:
|
121
|
+
|
122
|
+
- **Images**: Modifies the least significant bit of RGB color channels in a randomized order
|
123
|
+
- **Audio**: Modifies the least significant bit of audio sample data in a randomized pattern
|
124
|
+
|
125
|
+
## What is LSB and how does it work?
|
126
|
+
For a detailed explanation on LSB steganography and how it works, check out this article: https://kaizoku.gitbook.io/steganography
|
127
|
+
|
128
|
+
### Security Features
|
129
|
+
|
130
|
+
1. **Key-based Randomization**: Uses PRNG seeded with your secret key to randomize bit placement
|
131
|
+
2. **Automatic AES Encryption**: All data is encrypted using AES-EAX mode with SHA-256 key derivation
|
132
|
+
3. **Data Integrity Markers**: Uses start/end markers to ensure data completeness
|
133
|
+
4. **Format Validation**: Verifies PNG/WAV file formats before processing
|
134
|
+
5. **File Type Detection**: Automatically detects original file type using magic bytes for proper restoration
|
135
|
+
|
136
|
+
## Hidden Files
|
137
|
+
The tool can hide any file type and will automatically detect and restore the original format using magic byte signatures, including:
|
138
|
+
- Images: JPG, PNG, GIF, BMP, WebP, ICO
|
139
|
+
- Documents: PDF, DOC, ZIP archives
|
140
|
+
- Audio: MP3, OGG, FLAC, WAV
|
141
|
+
- Video: MP4, M4V, AVI
|
142
|
+
- Archives: ZIP, GZ, RAR, 7Z
|
143
|
+
- Text/Code: HTML, XML, Python, C, JavaScript, plain text
|
144
|
+
- Binary files: Any other format as .bin
|
145
|
+
|
146
|
+
|
147
|
+
## Limitations
|
148
|
+
|
149
|
+
- **Image capacity**: Limited by image size (3 bits per pixel for RGB images)
|
150
|
+
- **Audio capacity**: Limited by audio file length (1 bit per sample)
|
151
|
+
- **File size**: To hide larger files, you need larger cover media file
|
152
|
+
|
153
|
+
## Examples
|
154
|
+
|
155
|
+
### Hide a document in a photo
|
156
|
+
```bash
|
157
|
+
shadowbits img embed --in document.pdf --cover vacation.jpg --key family2023
|
158
|
+
```
|
159
|
+
|
160
|
+
### Extract the hidden document
|
161
|
+
```bash
|
162
|
+
shadowbits img extract --stego stego_file.png --key family2023
|
163
|
+
```
|
164
|
+
|
165
|
+
### Hide source code in music
|
166
|
+
```bash
|
167
|
+
shadowbits aud embed --in source_code.zip --cover favorite_song.mp3 --key coding123
|
168
|
+
```
|
169
|
+
|
170
|
+
## Security Considerations
|
171
|
+
|
172
|
+
- **Key Management**: Use strong, unique keys for each operation
|
173
|
+
- **Key Security**: The same key is used for both encryption and randomization
|
174
|
+
- **Cover Selection**: Choose cover files with sufficient capacity for your payload
|
175
|
+
- **File Format**: Ensure cover images are PNG and audio files are WAV
|
176
|
+
- **Key Reuse**: Avoid reusing keys across different files
|
177
|
+
|
178
|
+
## Error Handling
|
179
|
+
|
180
|
+
ShadowBits includes comprehensive error handling for:
|
181
|
+
- Invalid file formats
|
182
|
+
- Insufficient cover media capacity
|
183
|
+
- Corrupted embedded data or invalid markers
|
184
|
+
- AES Decryption failures (wrong key or corrupted data)
|
185
|
+
- Missing files or permissions
|
186
|
+
|
187
|
+
## Contributing
|
188
|
+
|
189
|
+
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
|
190
|
+
|
191
|
+
## License
|
192
|
+
|
193
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
194
|
+
|
195
|
+
## Disclaimer
|
196
|
+
|
197
|
+
This tool is for educational and legitimate purposes only. Users are responsible for ensuring compliance with applicable laws and regulations when using steganography techniques.
|
198
|
+
|
199
|
+
## Support
|
200
|
+
|
201
|
+
If you encounter any issues or have questions, please open an issue on GitHub.
|
202
|
+
|
203
|
+
---
|
204
|
+
|
205
|
+
**ShadowBits** - Where your secrets hide in plain sight 👁️🗨️
|
206
|
+
|
207
|
+
**Made by kaizoku**
|
@@ -0,0 +1,13 @@
|
|
1
|
+
shadowbits/cli.py,sha256=I9yQkazd9epOsbbPNjG8RWCVYzZCtWTErRX2PPP0xiQ,3315
|
2
|
+
shadowbits/emb_aud.py,sha256=5kGFYC-r59pWMrgnW3ktDc2h6-Ix_vfyrqGMC7-Dju0,3403
|
3
|
+
shadowbits/emb_img.py,sha256=XGZG9GA4za5Upu0oPgRpfh4k4cPBdgRjs6AvlDg4x0k,3382
|
4
|
+
shadowbits/ext_aud.py,sha256=SzXs97EedBu3v6iO25ZCLBRAV7tkYRa4XSP3shCx5eE,2592
|
5
|
+
shadowbits/ext_img.py,sha256=tgNOlnMVYUEOk3rTXFbekrGdk_emIuDw5ysD82eOlHw,3161
|
6
|
+
shadowbits/validator.py,sha256=uauRuxAY4KMfQJpxew-VKuMEx6DYu3wXfsHr0C5TlsY,3544
|
7
|
+
shadowbits/crypto/aes.py,sha256=J55J5PN89zqakhOxmAk-Zc6uQwCFifEXTAexxLAF5Q8,945
|
8
|
+
shadowbits-1.0.0.dist-info/licenses/LICENSE,sha256=iEyXbm1ZX-a9ro7ubAKtd4A7JsYWHqC6WTC5rWukeNs,1065
|
9
|
+
shadowbits-1.0.0.dist-info/METADATA,sha256=m0o93hk-rcKVw76nYlg3Aszvsbwi22PadODkOP89Q08,7306
|
10
|
+
shadowbits-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
shadowbits-1.0.0.dist-info/entry_points.txt,sha256=oXTWczfc9Y5-Fa7svu9DdEDj4qzEPNhmymENMRS1C5w,51
|
12
|
+
shadowbits-1.0.0.dist-info/top_level.txt,sha256=Di2Wya3HFPV4vkFBc8SAkbeWVPvRglnG_yE7ZaQOHzM,11
|
13
|
+
shadowbits-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 kaizoku73
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
shadowbits
|