Wulfie Reanimator Posted July 2, 2019 Share Posted July 2, 2019 (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 July 2, 2019 by Wulfie Reanimator 1 Link to comment Share on other sites More sharing options...
Oz Linden Posted July 2, 2019 Share Posted July 2, 2019 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. 1 2 Link to comment Share on other sites More sharing options...
Wulfie Reanimator Posted July 2, 2019 Author Share Posted July 2, 2019 (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 July 2, 2019 by Wulfie Reanimator Link to comment Share on other sites More sharing options...
Oz Linden Posted July 2, 2019 Share Posted July 2, 2019 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. Link to comment Share on other sites More sharing options...
panterapolnocy Posted July 2, 2019 Share Posted July 2, 2019 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(). Link to comment Share on other sites More sharing options...
Lucia Nightfire Posted July 3, 2019 Share Posted July 3, 2019 If we had Mod Keys we wouldn't have to use communication based appliers. 1 Link to comment Share on other sites More sharing options...
Mollymews Posted July 3, 2019 Share Posted July 3, 2019 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 Link to comment Share on other sites More sharing options...
Wulfie Reanimator Posted July 3, 2019 Author Share Posted July 3, 2019 (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 July 3, 2019 by Wulfie Reanimator Link to comment Share on other sites More sharing options...
Mollymews Posted July 4, 2019 Share Posted July 4, 2019 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 Link to comment Share on other sites More sharing options...
Mollymews Posted July 4, 2019 Share Posted July 4, 2019 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 ? 1 Link to comment Share on other sites More sharing options...
Recommended Posts
Please take a moment to consider if this thread is worth bumping.
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now