diff --git a/README.md b/README.md index 7ed6c2c..f17341b 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,4 @@ * [Image Transfer](./it.sh): a more convenient wrapper around dd for writing and reading removable media images * [Mount Manager](./mm.sh): a simple script to mount/unmount removable media partitions * [PNGizer](./pngizer.sh): an utility (and a shell library) to convert any file or directory to/from a PNG image +* [BitShade](./bitshade.sh): a steganographic tool for hiding any data file inside a PNG image diff --git a/bitshade.sh b/bitshade.sh new file mode 100755 index 0000000..ac5863e --- /dev/null +++ b/bitshade.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# BitShade: a simple POSIX-compatible tool for +# image-based steganography +# Depends on od, paste, dd, awk, sed, ImageMagick and pngcrush +# Usage: +# - encoding: bitshade.sh e [cover image] [file to hide] [output PNG] +# - decoding: bitshade.sh d [container PNG] [output file] +# Created by Luxferre in 2026, released into public domain + +# encode raw file as a netstring +to_netstring() { + printf '%d:' "$(cat "$1" | wc -c)" + cat "$1" + printf ',' +} + +# decode a raw file from a netstring (inside another file) +from_netstring() { + # get the container length + flen=$(head -n 1 "$1" | cut -f 1 -d :) + # skip the length itself and the : char + dd if="$1" of="$2" skip=$((${#flen} + 1)) bs=1 count=$flen +} + +# for every input byte, print its bits +conv_input() { + to_netstring "$1" | od -An -tu1 -v -w1 | awk '{n=int($0);for(i=0;i<8;i++){print(n%2);n=int(n/2)}}' +} + +# assemble a file bit by bit +collect_bits() { + outfile="$1" + tfile2=$(mktemp) + tfile3=$(mktemp) + awk 'BEGIN{n=0;b=1} {n+=b*int($1);b*=2;if(b==256){printf("\\%03o",n);n=0;b=1}}' > $tfile2 + printf "$(cat $tfile2)" > $tfile3 + from_netstring $tfile3 "$1" + rm -f $tfile2 $tfile3 +} + +# stego_encode cover_image hidden_text out.png +stego_encode() { + coverfile="$(mktemp)" + infile="$(mktemp)" + tfile="$(mktemp)" + temp_ppm="$(mktemp)" + magick $1 -compress none ppm:$temp_ppm + cat $temp_ppm | head -n 3 > $tfile + cat $temp_ppm | tail -n +4 | sed 's/\s\+/\n/g' > $coverfile + conv_input "$2" > $infile + paste $coverfile $infile | awk '{print(NF>1?int(int($1)/4)*4+int($2)*2+(rand()>0.5?1:0):$1)}' >> $tfile + magick ppm:$tfile png:$3 + pngcrush -ow $3 + rm -f $coverfile $infile $temp_ppm $tfile +} + +# stego_decode in.png out_file +stego_decode() { + temp_ppm="$(mktemp)" + magick $1 -compress none ppm:$temp_ppm + cat $temp_ppm | tail -n +4 | sed 's/\s\+/\n/g' | awk '{x=int($0)%4;print(x<2?0:1)}' | collect_bits "$2" + rm -f $temp_ppm +} + +# entry point + +if [ "$1" = "e" ]; then + [ -z "$2" ] && echo 'No cover file specified! Exiting...' && exit 1 + [ -z "$3" ] && echo 'No input file specified! Exiting...' && exit 1 + [ -z "$4" ] && echo "No output file specified! Exiting..." && exit 1 + stego_encode "$2" "$3" "$4" +elif [ "$1" = "d" ]; then + [ -z "$2" ] && echo 'No input file specified! Exiting...' && exit 1 + [ -z "$3" ] && echo "No output file specified! Exiting..." && exit 1 + stego_decode "$2" "$3" +else + echo 'Invalid mode! Use e for encoding or d for decoding' +fi +