#/usr/bin/env lua --[[ Steganography with Lua-GD Steganography is the technique of writing hidden messages in such a way that no one apart from the intended recipient knows of the existence of the message; this is in contrast to cryptography, where the existence of the message is clear, but the meaning is obscured. Generally a steganographic message will appear to be something else, like a shopping list, an article, a picture, or some other "cover" message. In the digital age, steganography works by replacing bits of useless or unused data in regular computer files (such as graphics, sound, text, HTML, or even floppy disks) with bits of different, invisible information. This hidden information can be plain text, cipher text or even images. A Simple Example If Alice wants to send a secret message to Bob through an insecure channel, she can use some encryption software (like GnuPG) to encrypt the message with Bob's public key. It's a good solution because no one unless Bob will be able to read the message. She can also sign the message so Bob will know that the message really comes from her. BUT, a potential attacker will know that a ciphered message was sent. If the attacker has control over the communication channel, he might block the message in some way that Bob will never receive it. If Alice also HIDES the ciphertext in an unsuspected piece of information (like a photo of her cat) the attacker will not detect it and the message will arrive to Bob. This program will help Alice to hide some arbitrary text in a PNG image by replacing the least significant bits of each color channel of some pixels with bits from the encrypted message. PNG or other loseless compression algorithm are mandatory here, since compressing the image with a lossy algorithm will destroy the stored information. The maximum length of the message is limited by the image's size (each byte needs 8 color channels or 2 pixels and 2 channels from the next pixel). So, the image must have at least "ceil((length+1)*8/3)" pixels (the extra byte is the NUL marker for the end of the string). So, if Alice's message is "Meet me in the secret place at nine o'clock.", she will encrypt and sign it to something like "PyJYDpz5LCOSHPiXDvLHmVzxLV8qS7EFvZnoo1Mxk+BlT+7lMjpQKs" (imagine Alice's cat walking in you keyboard :). This is the ciphertext that will be sent to Bob through the image. The following table shows what happens to the first eight pixels from the image when mixed to the first three bytes from the encrypted message: +-----+---+----------+-----------------+----------+ | Pix | C | Orig img | Message | New img | | # | | bits | Chr | Dec | Bin | bits | +-----+---+----------+-----+-----+-----+----------+ | | R | 01010010 | | | 0 | 01010010 | | 1 | G | 00101010 | | | 1 | 00101011 | |_____| B | 00010101 | | | 0 | 00010100 | | | R | 11100100 | P | 080 | 1 | 11100101 | | 2 | G | 00100100 | | | 0 | 00100100 | |_____| B | 01001111 | | | 0 | 01001110 | | | R | 01010010 | | | 0 | 01010010 | | 3 | G | 00101110 |_____|_____|__0__| 00101110 | |_____| B | 00111001 | | | 0 | 00111000 | | | R | 10010110 | | | 1 | 10010111 | | 4 | G | 01011101 | | | 1 | 01011101 | |_____| B | 00100101 | y | 121 | 1 | 00100101 | | | R | 01001001 | | | 1 | 01001001 | | 5 | G | 10110110 | | | 0 | 10110110 | |_____| B | 00010101 | | | 0 | 00010100 | | | R | 00110100 |_____|_____|__1__| 00110101 | | 6 | G | 01000111 | | | 0 | 01000110 | |_____| B | 01001000 | | | 1 | 01001001 | | | R | 01010110 | | | 0 | 01010110 | | 7 | G | 00011001 | | | 0 | 00011000 | |_____| B | 10010100 | J | 074 | 1 | 10010101 | | | R | 00010101 | | | 0 | 00010100 | | 8 | G | 01011010 | | | 1 | 01011011 | | | B | 01010001 | | | 0 | 01010000 | +-----+---+----------+-----+-----+-----+----------+ When Bob wants to read the message he will extract the least significant bit (LSB) from each color channel from some pixels of the image and join them to get the original ciphertext. A NULL character (ASCII #0) will mark the end of the message within the image, so he will know when to stop. Of course, this program will also do this boring job for Bob. $Id: steg.lua,v 1.19 2006/05/01 00:39:09 dermeister Exp $ --]] require "gd" function getLSB(n) return math.mod(n, 2) ~= 0 end -- Bizarre way to do some bit-level operations without bitlib. function setLSB(n, b) if type(b) == "number" then if b == 0 then b = false else b = true end end if getLSB(n) then if b then return n elseif n > 0 then return n - 1 else return n + 1 end else if not b then return n elseif n > 0 then return n - 1 else return n + 1 end end end function intToBitArray(n) local ret = {} local i = 0 while n ~= 0 do ret[i] = getLSB(n) n = math.floor(n/2) ret.size = i i = i + 1 end return ret end function printBitArray(a) local i for i = a.size,0,-1 do if a[i] then io.write("1") else io.write("0") end end end function mergeMessage(im, msg) local w, h = im:sizeXY() msg = msg .. string.char(0) local len = string.len(msg) if h * w < len * 8 then return nil end local x, y = 0, 0 local oim = gd.createTrueColor(w, h) local i = 1 local a2, c, nc, chr local a = {} local s, e = 1, 1 local rgb = {} while y < h do c = im:getPixel(x, y) rgb.r = im:red(c) rgb.g = im:green(c) rgb.b = im:blue(c) if i <= len and e - s < 3 then a2 = intToBitArray(string.byte(string.sub(msg, i, i))) for cnt = 7,0,-1 do a[e+7-cnt] = a2[cnt] end i = i + 1 e = e + 8 end if e - s > 0 then rgb.r = setLSB(rgb.r, a[s]) a[s] = nil s = s + 1 end if e - s > 0 then rgb.g = setLSB(rgb.g, a[s]) a[s] = nil s = s + 1 end if e - s > 0 then rgb.b = setLSB(rgb.b, a[s]) a[s] = nil s = s + 1 end nc = oim:colorResolve(rgb.r, rgb.g, rgb.b) oim:setPixel(x, y, nc) x = x + 1 if x == w then x = 0 y = y + 1 end end return oim, len*8, w*h end function getMessage(im) local msg = {} local w, h = im:sizeXY() local x, y = 0, 0 local a = {} local s, e = 1, 1 local b = 0 local c while y <= h do c = im:getPixel(x, y) a[e] = getLSB(im:red(c)) a[e+1] = getLSB(im:green(c)) a[e+2] = getLSB(im:blue(c)) e = e + 2 if e - s >= 7 then b = 0 for p = s, s+7 do b = b * 2 if a[p] then b = b + 1 end a[p] = nil end s = s + 8 if b == 0 then return table.concat(msg) else msg[#msg+1] = string.char(b) end end e = e + 1 x = x + 1 if x == w then x = 0 y = y + 1 end end return table.concat(msg) end function compare(fimg1, fimg2) local im1 = gd.createFromPng(fimg1) if not im1 then print("ERROR: " .. fimg1 .. " bad PNG data.") os.exit(1) end local im2 = gd.createFromPng(fimg2) if not im2 then print("ERROR: " .. fimg2 .. " bad PNG data.") os.exit(1) end local w1, h1 = im1:sizeXY() local w2, h2 = im2:sizeXY() if w1 ~= w2 or h1 ~= h2 then print("ERROR: Images have different sizes.") os.exit(1) end local oim = gd.createTrueColor(w1, h1) local x, y = 0, 0 local c1, c2, oc, f, fc while y < h1 do c1 = im1:getPixel(x, y) c2 = im2:getPixel(x, y) if im1:red(c1) ~= im2:red(c2) or im1:green(c1) ~= im2:green(c2) or im1:blue(c1) ~= im2:blue(c2) then oc = oim:colorResolve(im2:red(c2), im2:green(c2), im2:blue(c2)) oim:setPixel(x, y, oc) else f = math.floor((im1:red(c1) + im1:green(c1) + im1:blue(c1))/6.0) fc = oim:colorResolve(f, f, f) oim:setPixel(x, y, fc) end x = x + 1 if x == w1 then x = 0 y = y + 1 end end return oim end function usage() print("Usage:") print(" lua steg.lua hide ") print(" lua steg.lua show ") print(" lua steg.lua diff ") print("") print(" hide - Reads a message from stdin and saves into .") print(" show - Reads a message from and prints it to stdout.") print(" diff - Compares two images and writes the diff to .") print("") print(" WARNING: All files used here must be in the PNG format!") end if not arg[1] or not arg[2] then usage() os.exit(1) end if arg[1] == "show" then im = gd.createFromPng(arg[2]) if not im then print("ERROR: Bad image data.") os.exit(1) end io.write(getMessage(im)) os.exit(0) end if arg[1] == "hide" then if not arg[3] then usage() os.exit(1) end im = gd.createFromPng(arg[2]) if not im then print("ERROR: Bad image data.") os.exit(1) end print("Type your message and press CTRL+D to finish.") msg = io.read("*a") oim, l, t = mergeMessage(im, msg) if not oim then print("ERROR: Image is too small for the message.") os.exit(1) end if not oim:png(arg[3]) then print("ERROR: Failed to write output file.") os.exit(1) end print(string.format("DONE: %2.1f%% of the image used to store the message.", 100.0*l/t)) os.exit(0) end if arg[1] == "diff" then if not arg[3] and arg[4] then usage() os.exit(1) end oim = compare(arg[2], arg[3]) if not oim:png(arg[4]) then print("ERROR: Failed to write output file.") os.exit(1) end os.exit(0) end usage() os.exit(1)