Jump to content
Wulfie Reanimator

SHA1 for secure communication

Recommended Posts

Posted (edited)

Feeling a little inspired by a recent thread regarding texture appliers, I want to branch off with a little chat about the ever-so-popular topic of "security" when passing data like texture UUIDs. (But it doesn't need to be limited to textures.)

LSL comes with two hashing functions, MD5 and its better counterpart SHA-1. These are non-reversible, so knowing that they are being used, knowing the channel, or even seeing most of the code won't help you in figuring out what the input/UUID was.

We can easily pick a unique channel for the applier and the target object by having the same channel-generating code in both scripts. This same concept should work for other data like textures.

The HUD would simply say the texture UUID's hash on whatever common channel the objects are using, something like this:

default
{
    touch_start(integer n)
    {
        llSay(1, llSHA1String("0790fb68-a5a3-e43a-4214-1d97e42c687a"));
        // Actually sends "966a54b6335678130b34142fbefa31b77c27b1af"
    }
}

Now, the target object's script would have a global list of textures and would compare the hash results of each one to see if it should be applied. This is similar to how you might check for a correct password from an input field.

list textures = [ // These are not real textures.
"b082caf6-ee20-2e5d-52b7-1d9c0fc8c4a3",
"cf5148ef-7e99-d6c0-955d-761cc000bf28",
"7da46040-0d5c-4f26-e4b1-1de5c3a3f90d",
"f89e3a29-1338-85ae-536b-a1c09d27d026",
"0790fb68-a5a3-e43a-4214-1d97e42c687a" // the correct one
];

default
{
    state_entry()
    {
        llListen(1, "", "", "");
    }

    listen(integer channel, string name, key id, string message)
    {
        // Ignore sources that don't belong to the owner.
        if(llGetOwnerKey(id) != llGetOwner()) return;

        // Start going through the list. (In reverse here, but not necessary.)
        integer index = llGetListLength(textures);
        while(index--)
        {
            // Compare the received hash with the SHA1 of stored textures.
            if(message == llSHA1String(llList2String(textures, index)))
            {
                llOwnerSay("Match! " + (string)index);
                // Now you have the index of the valid texture to apply.
                // llSetTexture(llList2String(textures, index), ALL_SIDES);
                return; // Exit the loop/event
            }
        }
    }
}

An alternative way to do this would be to use two lists instead -- the original texture UUIDs, and their hashes in the same order. This would more than double the memory usage (because the hash is 40 characters while the original is 36), but would eliminate the need to re-hash the list every time and you could use llListFindList instead. It's a balance between size/speed and might not work at all for large lists.

I don't recall anybody mentioning hashing (unless they're also talking about decryption), or is this too vanilla? Do you have a better way? Thoughts?

Edited by Wulfie Reanimator
  • Thanks 1

Share this post


Link to post
Share on other sites

This is quite a bit more involved and expensive than it needs to be. You're essentially using the SHA1 value as an expensive index into a list; if both scripts have the same list of textures in the same order, just passing the index into the list is just as secure and much cheaper.

 

  • Like 1
  • Thanks 2

Share this post


Link to post
Share on other sites
Posted (edited)
1 hour ago, Oz Linden said:

This is quite a bit more involved and expensive than it needs to be. You're essentially using the SHA1 value as an expensive index into a list; if both scripts have the same list of textures in the same order, just passing the index into the list is just as secure and much cheaper.

Maybe so, at least for a simple (or overthought?) case like my first example. I was also thinking of cases where the HUD/sender doesn't necessarily have an identical(ly ordered) global list, or global lists at all in the case of using the LSL Preprocessor or just simple if-else statements (as you might normally use). Example below.

It may also work as an alternative to things like offsetting a unique channel on a per-product basis, as different products using different textures wouldn't respond to wrong hashes even if they were all communicated on the same channel. Another secure way is to just pass the name of the button, but this would conflict with other scripts on the same channel expecting the same messages.

To clarify a bit more:

if(message == "button 1")
{
    // Will react to unintended objects on the same channel
}

if(message == "0790fb68-a5a3-e43a-4214-1d97e42c687a")
{
    // Requires sensitive data to be sent
}

if(message == llSHA1String("0790fb68-a5a3-e43a-4214-1d97e42c687a"))
{
    // Only reacts to specific and secure data
}

And since you mentioned it, how expensive is SHA1'ing, or did you mean just relatively expensive to passing an index?

Edited by Wulfie Reanimator

Share this post


Link to post
Share on other sites
5 hours ago, Wulfie Reanimator said:

And since you mentioned it, how expensive is SHA1'ing

I have not tried to measure it, but it's not  free.

Share this post


Link to post
Share on other sites

I would personally keep two lists, one with UUIDs and one with pre-hashed SHA keys, generated from these UUIDs. Then you wouldn't need to have the loop with llSHA1String() in receiver script but just a regular index extraction with llListFindList().

Share this post


Link to post
Share on other sites

thinking about this a bit from a security pov

am not sure that obfuscating command/trigger data sent over an open channel serves as any kinda protection unless there is some random salt added in so that the same data can be encoded into different messages for transmission and decoded

e.g

encode(BLUE) = ABCDE. transmit(ABCDE). decode(ABCDE) = BLUE
encode(BLUE) = EWDWS. transmit(EWDWS). decode(EWDWS) = BLUE

when we don't add salt then BLUE always equals ABCDE. When so then a villain doesn't have to know that BLUE is the cmd/trigger. When the villain sends ABCDE then the decoder will trigger BLUE

Share this post


Link to post
Share on other sites
Posted (edited)
21 hours ago, Oz Linden said:

I have not tried to measure it, but it's not free.

I agree, but then we just know as much as each other.

2 hours ago, Mollymews said:

thinking about this a bit from a security pov

am not sure that obfuscating command/trigger data sent over an open channel serves as any kinda protection unless there is some random salt added in so that the same data can be encoded into different messages for transmission and decoded

e.g

encode(BLUE) = ABCDE. transmit(ABCDE). decode(ABCDE) = BLUE
encode(BLUE) = EWDWS. transmit(EWDWS). decode(EWDWS) = BLUE

when we don't add salt then BLUE always equals ABCDE. When so then a villain doesn't have to know that BLUE is the cmd/trigger. When the villain sends ABCDE then the decoder will trigger BLUE

That might be a concern if the function itself should be protected from being triggered (either by someone else or the object's owner, which would obviously require other kinds of measures). What I'm trying is to have a simple way to protect is the real data being transmitted. (As opposed to all of those custom encrypt/decrypt functions.)

SHA1 cannot be decoded. You'll never know that the input was BLUE, which might be important for example if BLUE was a kind of password, UUID or HTTP response. So going back to my original example, the worst case that could happen is that someone else knows the hash that can be used to apply a texture that works with the product, but because you would obviously be checking for owner and all the other basic things in your script, they cannot change someone else's products or re-use the UUID, or cheat some game system by spying into the data.

Edited by Wulfie Reanimator

Share this post


Link to post
Share on other sites

salted or not salted, the sender and receiver necessarily have to use the same codebook/encode/decode method

going with no salt

encode(RED) = CODE_A. transmit(CODE_A). decode(CODE_A) = RED
encode(GREEN) = CODE_B. transmit(CODE_B). decode(CODE_B) = GREEN
encode(BLUE) = CODE_C. transmit(CODE_C). decode(CODE_C) = BLUE

the most efficient method to do this is as Oz Linden mentioned. The transmitted code is the index of RED, GREEN or BLUE in the codebook

// sender

list colors = ["RED", "GREEN", "BLUE"];

integer index = llListFindList(colors, ["BLUE"]); // encode
        
llRegionSayTo(receiver_id, app_channel, (string)index); // transmit



// receiver

list colors = ["RED", "GREEN", "BLUE"];

listen(integer channel, string name, key id, string msg)
{
    integer index = (integer)msg:
    string color = llList2String(colors, index); // decode
}

in the no salt situation, the next most efficient method is as you Wulfie and panterapolnocy mention. The decoder also uses llListFindList

list colors = ["RED", "GREEN", "BLUE"];
list codes  = ["CODE_A", "CODE_B", "CODE_C"]; 

// encode
integer index = llListFindList(colors, ["BLUE"]);
string msg = llList2String(codes, index);

// transmit 
llRegionSayTo(receiver_id, app_channel, msg);

// decode
integer index = llListFindList(codes, [msg]);
string color = llList2String(colors, index);

 

i think that where we might use a one-way hash code like SHA1 is in a situation where available script memory in the sender is tight. Freeing memory by not having to store the codes in the sender

Share this post


Link to post
Share on other sites

i will open up here a conversation for anyone reading who might be wondering what salt is, in the context of this discussion

basically we encode some random data (salt) with our actual data and mix them together. Transmit the mixed message. Then decode the mix, extracting our actual data

sticking with the simplest method as discussed above, encoding the index as our actual data. And going with the simplest mixer to explain how this works

this mixer, as wrote below, is not crypto-secure. We would only do this to make our villain have to work a bit harder

i will code this in longhand to hopefully make clear what is happening

// our index for this explanatory purpose is stored in the low 16 bits of the 32-bit integer type
// 16-bit number range [0..65535] for index values
// the remaining high 16 bits we fill with random data (salt)


list colors ["RED", "BLUE", "GREEN"];

integer app_secret = 23279;  // a secret number (password). A value in the range [0..65535] known to both the sender and receiver


// encode

integer index = llListFindList(colors, ["BLUE"]);

integer salt = (integer)llFrand(65536.0); // salt is now some random value that fits into 16 bits

integer code = salt << 16; // move the salt into the 16 high bits of our code

code = code | index;  // move the index into the 16 low bits of our code

code = code ^ app_secret;  // mix (XOR) the code with the secret


transmit(code);

// decode

code = code ^ app_secret;  // unmix by XORing the code with the secret

integer index = code & 65535;  // retrieve the index from the 16 low bits by masking off the salt in the high bits

string color = llList2String(colors, index);

the value of app_secret cannot be greater than half the number of bits available. As when it is then XOR will on some values overflow the 32-bit code buffer, which when decoded will return invalid data


using a smaller range to show/explain how a single-round XOR mixer works

// using a 4-bit code.  Top 2 bits for salt - bottom 2 bits for index

secret = 3;  // some value in the range [0..3]. The range of 2 bits

index = 2; // the index of "BLUE". some value in the range [0..3]. The range of 2 bits 

salt = random(4);  // salt is 0, 1, 2 or 3

code = salt << 2;  // move the salt into the 2 high bits
// the result of this is that the value in code is one of: 0, 4, 8 or 12  

code = code | index;  // result: 0+2=2. 4+2=6. 8+2=10. 12+2=14 

code = code ^ secret; // result: 2^3=1. 6^3=4. 10^3=9. 14^3=13

// the value of BLUE is transmitted as a code, either 1, 4, 9 or 13. All of which will resolve to BLUE when decoded


// using secret = 1

salt = random(4);  //  noise is 0, 1, 2 or 3 
code = salt << 2;  // code is 0, 4, 8, 12
code = code | index;  // result: 0+2=2. 4+2=6. 8+2=10. 12+2=14
code = code ^ secret; // result: 2^1=3. 6^1=7. 10^1=11. 14^1=15

// BLUE is now encoded as 3, 7, 11 or 15 using the secret 1. All of which will resolve to BLUE when decoded

 

when we expand to the full 32 bits of integer type then our index of BLUE will be randomly encoded into 65536 different codes for any given secret. GREEN into 65536 codes, RED also.  Making it a little more difficult for our villain to work out what/which is BLUE, what/which is GREEN, and so on

whats the probability that the villain will crack a simple single-round XOR mixer like this one ? Arithmetically its 1 in 65536. The range of the secret

a thing. Before we do this kinda coding we first have to ask ourselves: What benefit do we gain which is worth the cost of doing it ?

  • Thanks 1

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...