This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
TheOddByte's profile picture

Problem with encrypting messages

Started by TheOddByte, 19 December 2014 - 12:43 PM
TheOddByte #1
Posted 19 December 2014 - 01:43 PM
Hey, I'm trying todo a secure rednet connection between two computers, the host and the client. When I tried this I noticed that sometimes it doesn't work, it seems the encryption gets a nil value. Here's the test scripts I used
Server

local function encrypt( _data, key )
	local data
	if type( _data ) == "table" then
   data = {}
   for k, v in pairs( _data ) do
   local index = AES.encrypt( key, k )
local value = AES.encrypt( key, v )
   data[base64.encode( index )] = base64.encode( value )
end

elseif type( _data ) == "string" then
   data = AES.encrypt( key, _data )
data = base64.encode( data )
end
return data
end

local function decrypt( data, key )
	if type( data ) == "table" then
   for k, v in pairs( data ) do
   local index = base64.decode( k )
local value = base64.decode( v )
   data[AES.decrypt( key, index )] = AES.decrypt( key, value )
end

elseif type( data ) == "string" then
   data = base64.decode( data )
data = AES.decrypt( key, data )
end
	return data
end

term.clear()
term.setCursorPos( 1, 1 )
while true do
	local e = { os.pullEvent( "rednet_message" ) }
local key = hash.sha256( "test" )
	local data = decrypt( e[3], key )
	local message = data["text"]
	print( "Received message: " .. message )
end
Client


local function encrypt( _data, key )
	local data
	if type( _data ) == "table" then
   data = {}
   for k, v in pairs( _data ) do
   local index = AES.encrypt( key, k )
local value = AES.encrypt( key, v )
   data[base64.encode( index )] = base64.encode( value )
end

elseif type( _data ) == "string" then
   data = AES.encrypt( key, _data )
data = base64.encode( data )
end
return data
end

local function decrypt( data, key )
	if type( data ) == "table" then
   for k, v in pairs( data ) do
   local index = base64.decode( k )
local value = base64.decode( v )
   data[AES.decrypt( key, k )] = AES.decrypt( key, v )
end

elseif type( data ) == "string" then
   data = base64.decode( data )
data = AES.decrypt( key, data )
end
	return data
end


local packet = {
	["text"] = "Hello World!";
}


local key = hash.sha256( "test" )
rednet.send( 0, encrypt( packet, key ) )
You should know that I'm using GravityScore's SHA256 script, and SquidDev's AES encryption API. I found the Base64 encoder here

This is the result I got from running the scripts above, don't say that it's because I'm using an emulator I'm getting this result.
I have tried it in-game as well, I got the same result there. ( and I don't know why it happens :/ )
SquidDev #2
Posted 19 December 2014 - 07:15 PM
I've done a bit of debugging with this. I'm using the un-minified version of Aes (DMx8M0LP). I'll post my findings so far, if I make any progress then I'll write add it to this:

At about line 1512 there is a function called decryptString. This is called by aes.decrypt. The trouble is, the string being passed is less than 16 characters long, resulting in the buffer never being filled, so nil is returned, so the validation fails, so everything blows up.

16 characters are passed as the value is never b64 decoded. Hmm.


UPDATE:
I've found why. This is your current code:


local function decrypt( data, key )
  if type( data ) == "table" then
    for k, v in pairs( data ) do
      local index = base64.decode( k )
      local value = base64.decode( v )
      data[AES.decrypt( key, index )] = AES.decrypt( key, value )  -- <== LOOK!
    end
  elseif type( data ) == "string" then
    data = base64.decode( data )
    data = AES.decrypt( key, data )
  end
  return data
end
The line kinda highlighted above shows the problem. You are assigning to the same table as before. So if the decrypted version occurs after the encrypted version in the table it attempts to decrypt it twice. Changing the output data to be called result, or dataResult or whatever would solve everything.
Edited on 19 December 2014 - 06:31 PM
TheOddByte #3
Posted 20 December 2014 - 12:04 PM
Thanks, it works without the tables getting corrupted now! :D/>
TheOddByte #4
Posted 20 December 2014 - 07:17 PM
Okay, another question. I don't know why but when I have connected the client to the server and try to send a message to it, it returns a nil message
Server

os.loadAPI( "hash" )
os.loadAPI( "AES" )
os.loadAPI( "base64" )


term.clear()
term.setCursorPos( 1, 1 )

rednet.open( "back" )
local SNet = dofile( "SNet" )

local network = SNet.host( "TsT", "server", "test" )

if network then
    print( "Server is now running" )
    while true do
       local e = { os.pullEvent( "rednet_message" ) }
       local id, message = network:handle( unpack( e ) )
       print( type( message ) ) --# It prints nil here 
       if id and type( message ) == "string" then
           local time = textutils.formatTime( os.time(), true )
           term.write( "[" .. time .. "][" .. id .. "] " .. message )
       end
    end
else
    error( "Failed to host server", 0 )
end
Client

rednet.open( "back" )

os.loadAPI( "hash" )
os.loadAPI( "AES" )
os.loadAPI( "base64" )


local SNet = dofile( "SNet" )


term.clear()
term.setCursorPos( 1, 1 )
print( "Connecting to server.." )
local server = SNet.connect( "TsT", "server", "test" )
if not server then
    error( "Couldn't connect to server", 0 )
end

while true do
    write( "> " )
    local input = read()
    server:send( server.id, input )
end
SNet API

--[[
    [API] SNet
@version 1.0, 2014-12-18
@author TheOddByte
--]]



local SNet = {}
SNet.__index = SNet



--# Check that all the needed APIs are loaded
assert( hash ~= nil, "hash API needed for this script" )
assert( base64 ~= nil, "base64 API needed for this script" )
assert( AES ~= nil, "AES API needed for this script" )




--# Local functions
local function exists( protocol, hostname )
    local id = rednet.lookup( protocol, hostname )
return type( id ) == "number" and true, id or false
end

local function encrypt( _data, key )
    local data
    if type( _data ) == "table" then
   data = {}
   for k, v in pairs( _data ) do
   local index = AES.encrypt( key, k )
local value = AES.encrypt( key, v )
   data[base64.encode( index )] = base64.encode( value )
end

elseif type( _data ) == "string" then
   data = AES.encrypt( key, _data )
data = base64.encode( data )
end
return data
end

local function decrypt( data, key )
    local result = {}
    if type( data ) == "table" then
   result = {}
   for k, v in pairs( data ) do
   local index = base64.decode( k )
index = AES.decrypt( key, index )
local value = base64.decode( v )
value = AES.decrypt( key, value )
   result[index] = value
end

elseif type( data ) == "string" then
   result = base64.decode( data )
result = AES.decrypt( key, data )
end
    return result
end





SNet.connect = function( protocol, hostname, server_key )
    local valid, host_id = exists( protocol, hostname )
if valid then
local net = {
isHost   = false;
key      = hash.sha256( tostring( math.ceil(os.time())*host_id ) );
id       = host_id;
protocol = protocol;
hostname = hostname;
}
local packet = {
   ["request"] = "snet.connect";
["seed"] = tostring( os.time()*math.random() );
}
net.key = hash.sha256( tostring( packet["seed"]*host_id ) )

--# Share the seed with the server
rednet.send( host_id, encrypt( packet, server_key ), protocol )

--# Get the response from the server
local id, packet
local time, timer, timeout = 0, os.startTimer( 1 ), 10
repeat
   id, packet = rednet.receive( protocol, 1 )
time = time + 1
until id == host_id and type( packet ) == "table" or time == timeout 
if id == host_id then
   if packet["response"] and packet["response"] == "success" then
       return setmetatable( net, SNet )
end
end
end
return nil
end


SNet.host = function( protocol, hostname, key )
    if not exists( protocol, hostname ) then
   local net = {
   key         = key;
   isHost      = true;
   connections = {};
protocol    = protocol;
hostname    = hostname;
}
rednet.host( protocol, hostname )
return setmetatable( net, SNet )
end
return nil
end


function SNet:isValidID( id )
    if self.isHost then
   if self.connections[tostring(id)] then
   return true;
end
else
   if id == self.id then
   return true;
end
end
return false
end


function SNet:handle( ... )
    local e = { ... }
if e[1] == "rednet_message" then
   if self.isHost then 
   if self:isValidID( e[2] ) and e[4] == self.protocol then
       return e[2], decrypt( e[3], self.connections[tostring(e[2])] ) --# It's here that it sends the message, it's here something goes wrong
else
   if e[4] == self.protocol then
local packet = decrypt( e[3], self.key )
if type( packet ) == "table" and packet["request"] and packet["request"] == "snet.connect" then
   print( "\nAdded " .. e[2] .. "\n" )
self.connections[tostring( e[2] )] = hash.sha256( tostring( packet["seed"]*os.getComputerID() ) )
self:send( e[2],{
   ["response"] = "success";
})
return nil
end
end
end
else
   if self:isValidID( e[2] ) then
   return e[2], decrypt( e[3], self.key )
end
end
else
   return nil
end
end


function SNet:send( id, message )
    if self:isValidID( id ) then
   if self.isHost then
            rednet.send( id, encrypt( message, self.connections[tostring(id)] ), self.protocol )
else
   rednet.send( id, encrypt( message, self.key ), self.protocol )
end
end
end



function SNet:receive( timeout )
    if timeout then
   local time, timer, e = 0, os.startTimer( 1 )
repeat
            e = { os.pullEvent() }
if e[1] == "timer" and e[2] == timer then
   time = time + 1
end
until e[1] == "rednet_message" and self:isValidID( e[2] ) and e[4] == self.protocol or time == timeout
if e[1] == "rednet_message" then
return e[2], decrypt( message, self.key )
else
   return nil
end
else
   local e, message
   repeat
e = { os.pullEvent( "rednet_message" ) }
if e[1] == "rednet_message" then
message = decrypt( message, self.key )
end
until self:isValidID( e[2] ) and e[4] == self.protocol
return e[2], message
end
end


function SNet:disconnect()
    self:send( self.id, {
   ["request"] = "snet.disconnect";
})
end



return SNet
Anyone have a clue what the problem is here?
Bomb Bloke #5
Posted 21 December 2014 - 07:23 AM
I haven't read all your code, but the SNet:receive() function stood out to me as a bit of a mess; if a timeout higher than one second is specified, it'll fail to respect it (as you only ever run one timer for one second, then fail to start a new one when it ends). Either way, it never tries to decrypt the right variable. You could re-write it like so:

function SNet:receive( timeout )
	if type(timeout) == "number" then timeout = os.startTimer(timeout) end
	
	while true do
		local e = { os.pullEvent() }
		if e[1] == "rednet_message" and self:isValidID( e[2] ) and e[4] == self.protocol then
			return e[2], decrypt( e[3], self.key )
		elseif e[1] == "timer" and e[2] == timeout then return nil end
	end
end

Regarding the problem at hand, I'd stick print statements into the decrypt function to 1) determine if it's getting called when you think it's getting called 2) indicate what data's being passed to it 3) indicate what data it thinks it's passing back. Depending on what the results tell you, you should be able to track down where the logic jumps off the expected rails.
TheOddByte #6
Posted 21 December 2014 - 12:49 PM
I haven't read all your code, but the SNet:receive() function stood out to me as a bit of a mess; if a timeout higher than one second is specified, it'll fail to respect it (as you only ever run one timer for one second, then fail to start a new one when it ends). Either way, it never tries to decrypt the right variable. You could re-write it like so:

function SNet:receive( timeout )
	if type(timeout) == "number" then timeout = os.startTimer(timeout) end
	
	while true do
		local e = { os.pullEvent() }
		if e[1] == "rednet_message" and self:isValidID( e[2] ) and e[4] == self.protocol then
			return e[2], decrypt( e[3], self.key )
		elseif e[1] == "timer" and e[2] == timeout then return nil end
	end
end

Regarding the problem at hand, I'd stick print statements into the decrypt function to 1) determine if it's getting called when you think it's getting called 2) indicate what data's being passed to it 3) indicate what data it thinks it's passing back. Depending on what the results tell you, you should be able to track down where the logic jumps off the expected rails.
Well thanks, But I've never really used that function yet as I planned on fixing it later. The real problems is in the SNet:handle function. It seems something goes wrong with the encryption there and it returns a nil message on the server when decrypting.
TheOddByte #7
Posted 21 December 2014 - 06:26 PM
Just found out what was wrong

local function decrypt( data, key )
	local result = {}
	if type( data ) == "table" then
   result = {}
   for k, v in pairs( data ) do
   local index = base64.decode( k )
index = AES.decrypt( key, index )
local value = base64.decode( v )
value = AES.decrypt( key, value )
   result[index] = value
end

elseif type( data ) == "string" then
   result = base64.decode( data )
result = AES.decrypt( key, data ) -- this line of code was trying to decrypt the base64 encoded string
end
	return result
end


local function decrypt( data, key )
	local result = {}
	if type( data ) == "table" then
   result = {}
   for k, v in pairs( data ) do
   local index = base64.decode( k )
index = AES.decrypt( key, index )
local value = base64.decode( v )
value = AES.decrypt( key, value )
   result[index] = value
end

elseif type( data ) == "string" then
   result = base64.decode( data )
result = AES.decrypt( key, result ) -- now that's more like it :3
end
	return result
end
Thanks for the help everyone, and thanks for cleaning up my code Bomb Bloke ^^

Edited on 21 December 2014 - 05:30 PM