├── doc ├── lenna.png ├── DCTlenna.png ├── LSBlenna.png ├── lenna_all.png ├── original_vs_dct.png ├── original_vs_lsb.png ├── stego_finalreport.pdf ├── original_vs_original.png ├── steg-proposal.tex └── stego_finalreport.tex ├── img ├── lenna.png ├── DCTlenna.png └── LSBlenna.png ├── requirements.txt ├── compare.py ├── README.md ├── .gitignore ├── stego.py ├── LSB.py └── DCT.py /doc/lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/lenna.png -------------------------------------------------------------------------------- /img/lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/img/lenna.png -------------------------------------------------------------------------------- /doc/DCTlenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/DCTlenna.png -------------------------------------------------------------------------------- /doc/LSBlenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/LSBlenna.png -------------------------------------------------------------------------------- /doc/lenna_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/lenna_all.png -------------------------------------------------------------------------------- /img/DCTlenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/img/DCTlenna.png -------------------------------------------------------------------------------- /img/LSBlenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/img/LSBlenna.png -------------------------------------------------------------------------------- /doc/original_vs_dct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/original_vs_dct.png -------------------------------------------------------------------------------- /doc/original_vs_lsb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/original_vs_lsb.png -------------------------------------------------------------------------------- /doc/stego_finalreport.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/stego_finalreport.pdf -------------------------------------------------------------------------------- /doc/original_vs_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neivin/stego/HEAD/doc/original_vs_original.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openCV==3.0.0 2 | cycler==0.10.0 3 | decorator==4.0.11 4 | matplotlib==2.0.0 5 | networkx==1.11 6 | numpy==1.12.1 7 | olefile==0.44 8 | Pillow==4.1.0 9 | pyparsing==2.2.0 10 | python-dateutil==2.6.0 11 | pytz==2017.2 12 | PyWavelets==0.5.2 13 | scikit-image==0.13.0 14 | scipy==0.19.0 15 | six==1.10.0 16 | -------------------------------------------------------------------------------- /compare.py: -------------------------------------------------------------------------------- 1 | from skimage.measure import compare_ssim as structSim 2 | import matplotlib.pyplot as plot 3 | import numpy 4 | import cv2 5 | 6 | 7 | def meanSquareError(im1, im2): 8 | error = numpy.sum((im1.astype('float') - im2.astype('float')) ** 2) 9 | error /= float(im1.shape[0] * im1.shape[1]); 10 | 11 | return error 12 | 13 | def compareImages(im1, im2, title): 14 | mse = meanSquareError(im1,im2) 15 | ss = structSim(im1,im2,multichannel=True) 16 | 17 | # make figure 18 | fig = plot.figure(title) 19 | plot.suptitle('MSE: %.2f, SSIM: %.2f' % (mse,ss)) 20 | 21 | ax = fig.add_subplot(1,2,1) 22 | plot.imshow(im1, cmap = plot.cm.gray) 23 | plot.axis('off') 24 | 25 | ax = fig.add_subplot(1,2,2) 26 | plot.imshow(im2, cmap = plot.cm.gray) 27 | plot.axis('off') 28 | 29 | plot.show() 30 | 31 | def main(): 32 | original = cv2.imread('img/lenna.png') 33 | lsbEncoded = cv2.imread('img/LSBlenna.png') 34 | dctEncoded = cv2.imread('img/DCTlenna.png') 35 | 36 | original = cv2.cvtColor(original, cv2.COLOR_BGR2RGB) 37 | lsbEncoded = cv2.cvtColor(lsbEncoded, cv2.COLOR_BGR2RGB) 38 | dctEncoded = cv2.cvtColor(dctEncoded, cv2.COLOR_BGR2RGB) 39 | 40 | # figure 41 | fig = plot.figure("Images") 42 | images = ('Original', original), ('LSB', lsbEncoded), ('DCT', dctEncoded) 43 | 44 | for (i, (name, image)) in enumerate(images): 45 | ax = fig.add_subplot(1,3,i+1) 46 | ax.set_title(name) 47 | plot.imshow(image, cmap=plot.cm.gray) 48 | plot.axis('off') 49 | 50 | plot.show() 51 | 52 | # Compare all the images 53 | compareImages(original, original, "Original vs Original") 54 | compareImages(original, lsbEncoded, "Original vs LSB") 55 | compareImages(original, dctEncoded, "Original vs DCT") 56 | 57 | 58 | if __name__=='__main__': 59 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Comparison of Image Steganography Techniques (LSB vs DCT) 2 | 3 | This project was created for CIS*4110: Computer Security at the University of Guelph. 4 | 5 | ## Requirements 6 | 7 | The main requirements are: OpenCV & Pillow. Comparing the results requires matplotlib and other dependencies. 8 | 9 | - openCV==3.0.0 10 | - cycler==0.10.0 11 | - decorator==4.0.11 12 | - matplotlib==2.0.0 13 | - networkx==1.11 14 | - numpy==1.12.1 15 | - olefile==0.44 16 | - Pillow==4.1.0 17 | - pyparsing==2.2.0 18 | - python-dateutil==2.6.0 19 | - pytz==2017.2 20 | - PyWavelets==0.5.2 21 | - scikit-image==0.13.0 22 | - scipy==0.19.0 23 | - six==1.10.0 24 | 25 | ## Usage 26 | Standard usage is: 27 | 28 | stego.py [-h] [-d] [-a] -i FILE [-o FILE] [-s STRING] [-f FILE] 29 | 30 | ``` 31 | Stego: DCT and LSB Image Steganography 32 | 33 | Optional arguments: 34 | -h, --help Show this help message and exit 35 | -d Set method to decode, default is encode 36 | -a Set encoding/decoding algorithm to LSB, default is DCT 37 | -i FILE Specify input file name 38 | -o FILE Specify output file name (optional) 39 | -s STRING Specify message to encrypt 40 | -f FILE Specify text file containing message 41 | ``` 42 | 43 | LSB encryption example: 44 | 45 | stego.py -i inputFile.jpg -a -s 'message to encrypt' 46 | 47 | DCT encryption example: 48 | 49 | stego.py -i inputFile.jpg -s 'message to encrypt' 50 | 51 | LSB decryption example: 52 | 53 | stego.py -i inputFile.jpg -a -d 54 | 55 | DCT decryption example: 56 | 57 | stego.py -i inputFile.jpg -d 58 | 59 | ## Contributors 60 | - [Neivin Mathew](https://github.com/neivin) 61 | - [Robyn Rintjema](https://github.com/rrintjem) 62 | - [Steven Kalapos](https://github.com/skalapos) 63 | 64 | -------------------------------------------------------------------------------- /doc/steg-proposal.tex: -------------------------------------------------------------------------------- 1 | \documentclass[letterpaper]{article} 2 | 3 | \usepackage[utf8]{inputenc} 4 | \usepackage[margin=1.75in]{geometry} 5 | \usepackage{setspace} 6 | \linespread{1.5} 7 | 8 | \pagestyle{empty} 9 | 10 | \title{Computer Security Project Proposal \\ 11 | A Comparison of Image Steganography Techniques} 12 | \author{ 13 | Neivin Mathew, 14 | Robyn Rintjema, 15 | Steven Kalapos 16 | } 17 | 18 | 19 | \begin{document} 20 | \maketitle 21 | \thispagestyle{empty} 22 | 23 | Steganography is the practice of concealing a file, image, or message within another file, image or message. While cryptography attempts to obfuscate a message into a cipher that is unreadable to the human eye, steganography aims to hide a secret message in plain sight. The advantage of steganography over cryptography is that the secret message does not attract attention to itself as an object of interest. Due to this nature, even if an attacker obtains concealed sensitive data, they might not only be unaware that they possess some secret information, but also not know what algorithm was used to conceal it.\\ 24 | 25 | The objective of this project will be to compare two different steganography techniques. It will analyze a Spatial Domain technique known as the Least Significant Bit (LSB) technique, and a Transform Domain technique called the Discrete Cosine Transform (DCT) technique. LSB is a simple method of concealing data in the least significant bits of pixel values without altering the cover image too much. DCT involves embedding a message in an image that is transformed in the frequency domain, and the message bits are inserted into the transformed coefficients of the cover image.\\ 26 | 27 | This project will compare these two implementations for effectiveness, strength and the impact of hiding a message in the cover image. 28 | 29 | \end{document}\grid 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | ## Core latex/pdflatex auxiliary files: 4 | *.aux 5 | *.lof 6 | *.log 7 | *.lot 8 | *.fls 9 | *.out 10 | *.toc 11 | *.fmt 12 | *.fot 13 | *.cb 14 | *.cb2 15 | 16 | ## Intermediate documents: 17 | *.dvi 18 | *-converted-to.* 19 | # these rules might exclude image files for figures etc. 20 | # *.ps 21 | # *.eps 22 | # *.pdf 23 | 24 | ## Generated if empty string is given at "Please type another file name for output:" 25 | .pdf 26 | 27 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 28 | *.bbl 29 | *.bcf 30 | *.blg 31 | *-blx.aux 32 | *-blx.bib 33 | *.run.xml 34 | 35 | ## Build tool auxiliary files: 36 | *.fdb_latexmk 37 | *.synctex 38 | *.synctex(busy) 39 | .synctex.gz 40 | *.synctex.gz(busy) 41 | *.pdfsync 42 | 43 | ## Auxiliary and intermediate files from other packages: 44 | # algorithms 45 | *.alg 46 | *.loa 47 | 48 | # achemso 49 | acs-*.bib 50 | 51 | # amsthm 52 | *.thm 53 | 54 | # beamer 55 | *.nav 56 | *.pre 57 | *.snm 58 | *.vrb 59 | 60 | # changes 61 | *.soc 62 | 63 | # cprotect 64 | *.cpt 65 | 66 | # elsarticle (documentclass of Elsevier journals) 67 | *.spl 68 | 69 | # endnotes 70 | *.ent 71 | 72 | # fixme 73 | *.lox 74 | 75 | # feynmf/feynmp 76 | *.mf 77 | *.mp 78 | *.t[1-9] 79 | *.t[1-9][0-9] 80 | *.tfm 81 | 82 | #(r)(e)ledmac/(r)(e)ledpar 83 | *.end 84 | *.?end 85 | *.[1-9] 86 | *.[1-9][0-9] 87 | *.[1-9][0-9][0-9] 88 | *.[1-9]R 89 | *.[1-9][0-9]R 90 | *.[1-9][0-9][0-9]R 91 | *.eledsec[1-9] 92 | *.eledsec[1-9]R 93 | *.eledsec[1-9][0-9] 94 | *.eledsec[1-9][0-9]R 95 | *.eledsec[1-9][0-9][0-9] 96 | *.eledsec[1-9][0-9][0-9]R 97 | 98 | # glossaries 99 | *.acn 100 | *.acr 101 | *.glg 102 | *.glo 103 | *.gls 104 | *.glsdefs 105 | 106 | # gnuplottex 107 | *-gnuplottex-* 108 | 109 | # gregoriotex 110 | *.gaux 111 | *.gtex 112 | 113 | # hyperref 114 | *.brf 115 | 116 | # knitr 117 | *-concordance.tex 118 | # TODO Comment the next line if you want to keep your tikz graphics files 119 | *.tikz 120 | *-tikzDictionary 121 | 122 | # listings 123 | *.lol 124 | 125 | # makeidx 126 | *.idx 127 | *.ilg 128 | *.ind 129 | *.ist 130 | 131 | # minitoc 132 | *.maf 133 | *.mlf 134 | *.mlt 135 | *.mtc[0-9]* 136 | *.slf[0-9]* 137 | *.slt[0-9]* 138 | *.stc[0-9]* 139 | 140 | # minted 141 | _minted* 142 | *.pyg 143 | 144 | # morewrites 145 | *.mw 146 | 147 | # nomencl 148 | *.nlo 149 | 150 | # pax 151 | *.pax 152 | 153 | # sagetex 154 | *.sagetex.sage 155 | *.sagetex.py 156 | *.sagetex.scmd 157 | 158 | # scrwfile 159 | *.wrt 160 | 161 | # sympy 162 | *.sout 163 | *.sympy 164 | sympy-plots-for-*.tex/ 165 | 166 | # pdfcomment 167 | *.upa 168 | *.upb 169 | 170 | # pythontex 171 | *.pytxcode 172 | pythontex-files-*/ 173 | 174 | # thmtools 175 | *.loe 176 | 177 | # TikZ & PGF 178 | *.dpth 179 | *.md5 180 | *.auxlock 181 | 182 | # todonotes 183 | *.tdo 184 | 185 | # easy-todo 186 | *.lod 187 | 188 | # xindy 189 | *.xdy 190 | 191 | # xypic precompiled matrices 192 | *.xyc 193 | 194 | # endfloat 195 | *.ttt 196 | *.fff 197 | 198 | # Latexian 199 | TSWLatexianTemp* 200 | 201 | ## Editors: 202 | # WinEdt 203 | *.bak 204 | *.sav 205 | 206 | # Texpad 207 | .texpadtmp 208 | 209 | # Kile 210 | *.backup 211 | 212 | # KBibTeX 213 | *~[0-9]* 214 | 215 | # auto folder when using emacs and auctex 216 | /auto/* 217 | 218 | # expex forward references with \gathertags 219 | *-tags.tex 220 | -------------------------------------------------------------------------------- /stego.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | import sys 5 | import time 6 | from LSB import LSB 7 | from DCT import DCT 8 | from argparse import ArgumentParser 9 | 10 | def parser(): 11 | 12 | #set the command line arguments 13 | parser = ArgumentParser(description="Stego: DCT and LSB Image Steganography") 14 | 15 | parser.add_argument('-d', dest='encrypt', action='store_false', 16 | help="Set method to decode, default is encode", 17 | default=True) 18 | 19 | parser.add_argument('-a', dest='algorithm', action='store_const', 20 | help="Set encoding/decoding algorithm to LSB, default is DCT", 21 | const="LSB", default="DCT") 22 | 23 | parser.add_argument("-i", dest="inputfile", required=True, 24 | help="Specify input file name", metavar="FILE") 25 | 26 | parser.add_argument("-o", dest="outputfile", required=False, 27 | help="Specify output file name (optional)", metavar="FILE") 28 | 29 | parser.add_argument("-s", dest="string", required=False, 30 | help="Specify message to encrypt") 31 | 32 | parser.add_argument("-f", dest="file", required=False, 33 | help="Specify text file containing message", metavar="FILE") 34 | 35 | args = parser.parse_args() 36 | return args 37 | 38 | def main(): 39 | args = parser() 40 | 41 | algo = args.algorithm 42 | inFile = args.inputfile 43 | message = args.string 44 | outFile = args.outputfile 45 | msgFile = args.file 46 | 47 | 48 | #encryption input check 49 | if args.encrypt is True and args.string is None and args.file is None: 50 | raise ValueError("Encryption requires an input string") 51 | 52 | # read file msg 53 | if args.file is not None: 54 | with open(msgFile, 'r') as textFile: 55 | message = textFile.read().replace('\n', '') 56 | 57 | #encryption 58 | if args.encrypt: 59 | 60 | #set output file if not specified 61 | if not args.outputfile: 62 | rawName = os.path.basename(os.path.normpath(inFile)) 63 | dirName = os.path.dirname(os.path.normpath(inFile)) 64 | 65 | outFile = dirName + '/' + algo + rawName 66 | 67 | #LSB implementation 68 | if algo == "LSB": 69 | start = time.time() 70 | x = LSB(inFile) 71 | encoded = x.hide(message, outFile) 72 | end = time.time()-start 73 | print ('time: ') 74 | print (end) 75 | #print ('Message encoded = ' + x.message) 76 | else: 77 | #DCT implementation 78 | start = time.time() 79 | x = DCT(inFile) 80 | secret = x.DCTEn(message, outFile) 81 | end = time.time()-start 82 | print ('time: ') 83 | print (end) 84 | #print('Message encoded = '+ x.message) 85 | 86 | #decryption 87 | else: 88 | #LSB implementation 89 | if algo == 'LSB': 90 | start = time.time() 91 | y = LSB(inFile) 92 | secret = y.extract() 93 | end = time.time()-start 94 | print('Hidden Message:\n' + secret) 95 | print ('time: ') 96 | print (end) 97 | else: 98 | #DCT implementation 99 | start = time.time() 100 | y = DCT(inFile) 101 | decode = y.DCTDe() 102 | end = time.time()-start 103 | print('Hidden Message:\n'+ decode) 104 | print ('time: ') 105 | print (end) 106 | 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /LSB.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import sys 3 | 4 | 5 | class LSB(): 6 | def __init__(self, filename): 7 | self.filename = filename 8 | self.message = None 9 | self.cover = None 10 | self.bits = None 11 | 12 | def open_image(self): 13 | try: 14 | self.cover = Image.open(self.filename) 15 | return True 16 | except FileNotFoundError as e: 17 | print('Error: ' + self.filename + ' does not exist. Please specify an existing file') 18 | return False 19 | 20 | def get_bit_depth(self): 21 | mode_to_bd = {'1':1, 'L':8, 'P':8, 'RGB':24, 'RGBA':32, 'CMYK':32, 'YCbCr':24, 'I':32, 'F':32} 22 | 23 | if self.cover is not None: 24 | return mode_to_bd[self.cover.mode] 25 | 26 | 27 | def messageToBits(self, string): 28 | bits = [] 29 | 30 | # Convert each character into binary and pad with 0s 31 | for char in string: 32 | binval = bin(ord(char))[2:].rjust(8,'0') 33 | 34 | #for bit in binval: 35 | bits.append(binval) 36 | 37 | return bits 38 | 39 | """ Validates that the message can fit into the specified file 40 | Counts the number of bits in the secret message and 41 | compares it to how much space exists in the cover image """ 42 | def validate(self): 43 | img = self.open_image() 44 | 45 | # Find the capacity of the image 46 | capacity = 0 47 | if img: 48 | capacity = self.cover.width * self.cover.height * (self.get_bit_depth()/8) 49 | # print ('Capacity of image:\t' + str(capacity)) 50 | 51 | # Convert the string message into bits 52 | self.bits = ''.join(self.messageToBits(self.message)) 53 | #print('Message bits:\t\t' + str(len(self.bits))) 54 | 55 | if len(self.bits) >= capacity: 56 | print('Error: The message is too long to be encoded into the image ' + self.filename) 57 | return False 58 | 59 | return True 60 | 61 | 62 | def setComponentLSB(self,component, bit): 63 | blankLSB = component & ~1 64 | return blankLSB | int(bit) 65 | 66 | 67 | def hide(self, secretMessage, outFilename): 68 | # Add length of message to message 69 | self.message = str(len(secretMessage)) + ':' + secretMessage 70 | 71 | 72 | # Check that the message can fit inside the image 73 | if not self.validate(): 74 | print ('Error: Validation failed. Cannot encode message into image') 75 | return 76 | 77 | encodedImage = self.cover.copy() 78 | width, height = self.cover.size 79 | 80 | # Position within bits of message 81 | index = 0 82 | 83 | for row in range(height): 84 | for col in range(width): 85 | 86 | if index + 1 <= len(self.bits): 87 | 88 | (r,g,b) = encodedImage.getpixel((col, row)) 89 | 90 | r = self.setComponentLSB(r, self.bits[index]) 91 | 92 | if index + 2 <= len(self.bits): 93 | g = self.setComponentLSB(g, self.bits[index+1]) 94 | 95 | if index + 3 <= len(self.bits): 96 | b = self.setComponentLSB(b, self.bits[index+2]) 97 | 98 | # Set new pixel values 99 | encodedImage.putpixel((col,row),(r,g,b)) 100 | 101 | index += 3 102 | 103 | encodedImage.save(outFilename) 104 | 105 | return encodedImage 106 | 107 | 108 | def extract(self): 109 | img = self.open_image() 110 | width, height = self.cover.size 111 | 112 | buff = 0 113 | count = 0 114 | 115 | messageBits = [] 116 | msgSize = None 117 | 118 | for row in range(height): 119 | for col in range(width): 120 | 121 | # Go through each RGB component and pull out the LSB 122 | for component in self.cover.getpixel((col, row)): 123 | # Read the bit and push left to make 8 bit chunk 124 | buff += (component & 1) << (7 - count) 125 | count += 1 126 | 127 | # Convert 8 bit chunk to char and append 128 | if count == 8: 129 | messageBits.append(chr(buff)) 130 | buff = 0 # Reset buffer 131 | count = 0 # and count 132 | 133 | # If we read the separator last, set the message size 134 | if messageBits[-1] == ':' and msgSize is None: 135 | try: 136 | msgSize = int(''.join(messageBits[:-1])) 137 | except: 138 | pass 139 | 140 | # Return the message when bits read match size of message 141 | if len(messageBits) - len(str(msgSize)) - 1 == msgSize: 142 | return ''.join(messageBits)[len(str(msgSize))+1:] 143 | 144 | return '' 145 | 146 | -------------------------------------------------------------------------------- /DCT.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | from __future__ import print_function 3 | import cv2, sys, numpy as np, itertools 4 | 5 | from PIL import Image 6 | 7 | quant = np.array([[16,11,10,16,24,40,51,61], 8 | [12,12,14,19,26,58,60,55], 9 | [14,13,16,24,40,57,69,56], 10 | [14,17,22,29,51,87,80,62], 11 | [18,22,37,56,68,109,103,77], 12 | [24,35,55,64,81,104,113,92], 13 | [49,64,78,87,103,121,120,101], 14 | [72,92,95,98,112,100,103,99]]) 15 | 16 | class DCT(): 17 | def __init__(self, imPath): 18 | self.imPath = imPath 19 | self.message = None 20 | self.bitMess = None 21 | self.oriCol = 0 22 | self.oriRow = 0 23 | self.numBits = 0 24 | 25 | """Input: secret - secret message to be hidden 26 | outIm - name of the image you want to be output 27 | Function: takes message to be hidden and preforms dct stegonography to hide the image within the least 28 | significant bits of the DC coefficents. 29 | Output: writes out an image with the encoded message""" 30 | def DCTEn(self, secret, outIm): 31 | #load image for processing 32 | img = self.loadImage() 33 | if img is None: 34 | print("Error: File not found!") 35 | return 36 | 37 | self.message = str(len(secret))+'*'+secret 38 | self.bitMess = self.toBits() 39 | 40 | 41 | 42 | #get size of image in pixels 43 | row,col = img.shape[:2] 44 | self.oriRow, self.oriCol = row, col 45 | 46 | if((col/8)*(row/8)