#!/usr/bin/env python
####
##
## unisteg.py
## Unicode Steganography
##
####
## Copyright (c) 2008, Michael Katsevman
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##
##    * Redistributions of source code must retain the above copyright
##      notice, this list of conditions and the following disclaimer.
##    * Redistributions in binary form must reproduce the above copyright
##      notice, this list of conditions and the following disclaimer in the
##      documentation and/or other materials provided with the distribution.
##
## THIS SOFTWARE IS PROVIDED BY Michael Katsevman "AS IS" AND ANY
## EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL Michael Katsevman BE LIABLE FOR ANY
## DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
####
from unicodedata import normalize as norm
from optparse import OptionParser
from urllib import urlopen
from os.path import exists

def int2bin(n, count=7):
    return "".join([str((n >> y) & 1) for y in range(count-1, -1, -1)])

def str2bin(s):
    return list(''.join([int2bin(ord(c)) for c in s]))

def bin2str(b):
    s = '' 
    while len(b) > 6:
        s += chr(int(b[:7],2))
        b = b[7:]
    return s

def decomposed(u): return norm("NFD",u) # decomposed
def composed(u): return norm("NFC",u) # cannonical

def steg(plain,cover):
    """Takes in a plaintext string of 1s and 0s and hides it in unicode covertext"""
    plain = list(plain)
    #plain_bin = 
    #print plain_bin
    #print plain_bin
    stego = u''
    for c in cover:
        if c != decomposed(c) and len(plain) > 0:        
            if plain.pop(0) == '1':
                stego += decomposed(c)
            else:
                stego += c
        else:
            stego += c
    return stego

def unsteg(stego):
    """Takes the stego'd cyphertext and output plaintext"""
    plain_bin = ''
    plain = ''
    while len(stego) > 0:  
        if len(composed(stego[:2])) == 1: # 2 chars have been nfd'd
            plain_bin += '1'
            stego = stego[2:]
        elif decomposed(stego[0]) != composed(stego[0]): # character that is not the same in both forms
            plain_bin += '0'
            stego = stego[1:]
        else:
            stego = stego[1:]
    return plain_bin
   


def get_opts():
    usage = """Usage: %prog [options] <cyphertext |  < plaintext | covertext > >

Prints output to stdout by default."""
    op = OptionParser(usage)

    op.add_option('-s','--steg',dest='hide',
                  action="store_true",
                  help='Hide plaintext in covertext to produce cyphertext.')

    op.add_option('--url-plain',dest='url_plain',
                  type='str',
                  help='URL to retrieve plaintext from')
    op.add_option('--url-cover',dest='url_cover',
                  type='str',
                  help='URL to retrieve covertext from')

    op.add_option('--file-plain',dest='file_plain',
                  type='str',
                  help='File to retrieve plaintext from')
    op.add_option('--file-cover',dest='file_cover',
                  type='str',
                  help='File to retrieve covertext from')

    op.add_option('-b','--binary',dest='binary',
                  action="store_true", default=False,
                  help='Use if the plaintext is a string of 1s and 0s')                 

    op.add_option('-e','--encoding',dest='encoding',
                  type='str',
                  help='Encoding of the covertext, if not unicode. See Python codecs module for possible values.')

    op.add_option('-u','--unsteg',dest='hide',
                  action="store_false",
                  help='Derive plaintext from cyphertext.')

    op.add_option('--url-steg',dest='url_steg',
                  type='str',
                  help='URL to retrieve cyphertext from')
    op.add_option('--file-steg',dest='file_steg',
                  type='str',
                  help='File to retrieve cyphertext from')

    op.add_option('-o','--out',dest='out',
                  type='str',
                  help='Filename of output')


    options,args = op.parse_args()

    if options.hide == None:
        op.error("Please choose either --steg or --unsteg operation.")

    if options.hide: # steg
        if len(args)<1:
            if not options.url_plain and not options.file_plain:
                op.error("Please provide a plaintext.")

        if len(args)<2:
            if not options.url_cover and not options.file_cover:
                op.error("Please provide a covertext.")
        
        if len(args)>0:
            if (options.url_plain and args[0]) or (options.file_plain and args[0]) or (options.url_plain and options.file_plain):
                op.error("Please provide only one plaintext source.")

        if len(args)>1:
            if (options.url_cover and args[1]) or (options.file_cover and args[1]) or (options.file_cover and options.url_cover):
                op.error("Please provide only one covertext source.")
    else: # unsteg
        if len(args)<1:
            if not options.url_steg and not options.file_steg:
                op.error("Please provide a cyphertext.")
        if len(args)>1:
            if (options.url_steg and args[0]) or (options.file_steg and args[0]) or (options.url_steg and options.file_steg):
                op.error("Please provide only one cyphertext source.")

    for t in [options.file_plain,options.file_cover,options.file_steg]:
            if t and not exists(t):
                op.error("File %s does not exist."%t)

            
    return (options,args)

def demo():
    test_cover2 = u"Sedat Bodrum'da tatil yap\u0131yor. Ama Bodrum'da hava so\u011fuk. Levent Bodrum'da de\u011fil. O \u0130stanbul'da. \u0130stanbul'da hava s\u0131cak. Levent tatil yapm\u0131yor. O i\u015fe gidiyor. Sedat i\u015fe gitmiyor, \xe7\xfcnk\xfc o bir \xf6\u011frencidir. Sedat telefonda Levent'lekonu\u015fuyor."
    test_cover = urlopen('http://www.theholyquran.org/sura_print.php?kid=1&sid=2').read().decode('iso-8859-9')
    print bin2str(unsteg(steg(str2bin('this is a test'),test_cover)))



if __name__=='__main__':
    options,args = get_opts()

    if options.hide: # we're doing steg hide
        plain = ''
        cover = ''
        if len(args)>0:
            plain = args[0]
        elif options.url_plain:
            plain = urlopen(options.url_plain).read()
        elif option.file_plain:
            plain = open(option.file_plain).read()

        if not options.binary:
            plain = str2bin(plain)

        if len(args)>1:
            cover = args[1]
        elif options.url_cover:
            cover = urlopen(options.url_cover).read()
        elif option.file_cover:
            cover = open(option.file_cover).read()

        if options.encoding:
            cover = cover.decode(options.encoding)

        out = steg(plain,cover)
  
    else: # unsteg
        stego = ''
        if len(args)>0:
            stego = args[0]
        elif options.url_steg:
            stego = urlopen(options.url_steg).read()
        elif options.file_steg:
            stego = open(options.file_steg).read()


        stego = stego.decode('utf-8')

        out = bin2str(unsteg(stego))
        for c in out:
            if ord(c) not in range(128):
                out = out[:out.find(c)]
                break  

    if options.out:
        
        open(options.out,'w').write(out.encode('utf-8'))
        print "---- Output written to file: %s ----"%options.out
    else:
        print out

    
    
    

