#/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