15
\$\begingroup\$

According to MDN, every WebSocket message is sent as one or more frames. Your job is to extract the payload given a (masked) client-to-server text frame. The steps to extract the payload is as follows:

  • ignore byte 1; read byte 2
    • if it is 254, skip the next 2 bytes
    • if it is 255, skip the next 8 bytes,
    • otherwise, continue
  • interpret the next 4 bytes as an XOR-cipher key
  • decrypt the final bytes using the (repeating) key and interpret it as a UTF-8 encoded string

For reference, here is an ungolfed solution:

#!/usr/bin/env python3
frame = open(0, 'rb').read()

pos = 2
if frame[1] == 254:
    pos += 2 # skip 2 bytes
if frame[1] == 255:
    pos += 8 # skip 8 bytes

# read the key (4 bytes)
key = frame[pos:][:4]
pos += 4

# decode the payload
payload = bytes(x ^ key[i % 4] for i, x in enumerate(frame[pos:]))

# output
print(payload.decode("utf-8"))

Test Cases

frame (octets are represented in hex) payload
81 83 3D 54 23 06 70 10 6D MDN
81 85 3D 54 23 06 55 31 4F 6A 52 hello
81 FE 01 BD 3D 54 23 06 71 3B 51 63 50 74 4A 76 4E 21 4E 26 59 3B 4F 69 4F 74 50 6F 49 74 42 6B 58 20 0F 26 5E 3B 4D 75 58 37 57 63 49 21 51 26 5C 30 4A 76 54 27 40 6F 53 33 03 63 51 3D 57 2A 1D 27 46 62 1D 30 4C 26 58 3D 56 75 50 3B 47 26 49 31 4E 76 52 26 03 6F 53 37 4A 62 54 30 56 68 49 74 56 72 1D 38 42 64 52 26 46 26 58 20 03 62 52 38 4C 74 58 74 4E 67 5A 3A 42 26 5C 38 4A 77 48 35 0D 26 68 20 03 63 53 3D 4E 26 5C 30 03 6B 54 3A 4A 6B 1D 22 46 68 54 35 4E 2A 1D 25 56 6F 4E 74 4D 69 4E 20 51 73 59 74 46 7E 58 26 40 6F 49 35 57 6F 52 3A 03 73 51 38 42 6B 5E 3B 03 6A 5C 36 4C 74 54 27 03 68 54 27 4A 26 48 20 03 67 51 3D 52 73 54 24 03 63 45 74 46 67 1D 37 4C 6B 50 3B 47 69 1D 37 4C 68 4E 31 52 73 5C 20 0D 26 79 21 4A 75 1D 35 56 72 58 74 4A 74 48 26 46 26 59 3B 4F 69 4F 74 4A 68 1D 26 46 76 4F 31 4B 63 53 30 46 74 54 20 03 6F 53 74 55 69 51 21 53 72 5C 20 46 26 4B 31 4F 6F 49 74 46 75 4E 31 03 65 54 38 4F 73 50 74 47 69 51 3B 51 63 1D 31 56 26 5B 21 44 6F 5C 20 03 68 48 38 4F 67 1D 24 42 74 54 35 57 73 4F 7A 03 43 45 37 46 76 49 31 56 74 1D 27 4A 68 49 74 4C 65 5E 35 46 65 5C 20 03 65 48 24 4A 62 5C 20 42 72 1D 3A 4C 68 1D 24 51 69 54 30 46 68 49 78 03 75 48 3A 57 26 54 3A 03 65 48 38 53 67 1D 25 56 6F 1D 3B 45 60 54 37 4A 67 1D 30 46 75 58 26 56 68 49 74 4E 69 51 38 4A 72 1D 35 4D 6F 50 74 4A 62 1D 31 50 72 1D 38 42 64 52 26 56 6B 13 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Standard loopholes are forbidden. Shortest code wins.

\$\endgroup\$
7
  • \$\begingroup\$ Hello, welcome to Code Golf, and very nice first question! \$\endgroup\$ Dec 26, 2022 at 2:25
  • \$\begingroup\$ Maybe you should add more testcases so at least one for 254, one for 255, one for otherwise. \$\endgroup\$
    – tsh
    Dec 26, 2022 at 2:28
  • \$\begingroup\$ Thanks for the feedback @tsh! The steps are simplified because the input assumes a complete, valid WebSocket frame rather than a buffered HTTP socket. However, the protocol uses the "skipped bytes" to represent the payload length. So, while technically a valid frame, a test case for 255 implies a payload-length greater than 2¹⁶-1. I'll add a test case for 254, though. \$\endgroup\$
    – sheared
    Dec 26, 2022 at 2:58
  • \$\begingroup\$ Looks like the 2nd byte is length|0x80, what does the first bit do? (unrelated to challenge) \$\endgroup\$
    – l4m2
    Dec 26, 2022 at 3:26
  • \$\begingroup\$ @l4m2 The lower 7 bits of byte 2 specify the payload length. For payloads greater than 126 bytes, byte 2 is set to 0xFE and the next 2 bytes are interpreted as an unsigned short and used as the payload length. When an unsigned short is still inadequate to express the payload length, byte 2 is set to 0xFF and the next 8 bytes are interpreted as an unsigned long. Furthermore, messages can be longer if sent across multiple frames, but that is out of the scope of this problem. \$\endgroup\$
    – sheared
    Dec 26, 2022 at 4:03

7 Answers 7

12
\$\begingroup\$

JavaScript (Node.js), 60 bytes

a=>a.slice(K=[8,14][a[1]^255]^6).map((n,i)=>n^a[K-4+i%4])+''

Try it online!

Input a Buffer, output string.

Or 83 bytes in browser, which accept Uint8Array as input.

\$\endgroup\$
4
\$\begingroup\$

J, 44 42 35 bytes

-2 9 bytes thanks to Bubbler!

4(}.]u:@XOR#$4$])}.}.~3^1-254*@-1&{

Try it online!

Expects a list of numeric bytes as input.

Explanation

4(}.]u:@XOR#$4$])}.}.~3^1-254*@-1&{
                             *@-       sign of difference between
                          254          254
                                1&{    and the second byte
                      3^1-             transform to the correct start byte - 1
                   }.~                 chop off that many bytes from
                 }.                    the head of the input
 (  ------------)                      monadic forks
             4$]                       select the key
           #$                          repeat that to the length of entire message
    ]   XOR                            bitwise xor with the entire message
     u:@                               convert to characters
4 {.                                   remove xor'd key
\$\endgroup\$
4
  • \$\begingroup\$ 4 2 10{~ can be changed to 1+3^1-. \$\endgroup\$
    – Bubbler
    Dec 28, 2022 at 1:05
  • \$\begingroup\$ @Bubbler thanks! \$\endgroup\$ Dec 28, 2022 at 1:11
  • 1
    \$\begingroup\$ 22 b. is XOR. Avoiding dyadic inner train seems to be shorter: 4(}.]u:@XOR#$4$])}.}.~3^1-254*@-1&{ (35 bytes) \$\endgroup\$
    – Bubbler
    Dec 28, 2022 at 1:30
  • \$\begingroup\$ @Bubbler Interesting approach, thanks again! \$\endgroup\$ Dec 28, 2022 at 3:24
4
\$\begingroup\$

Python,  101  83 bytes (@sheared)

thanks to @tsh for golfing the decryption part and the neat dict work!

def n(f):f=f[(*[2]*254,4,10)[f[1]]:];return[x^m for x,m in zip(f[4:],f[:4]*len(f))]

Attempt This Online!

\$\endgroup\$
7
  • \$\begingroup\$ Hi there, welcome to code golf! This is a pretty good first answer, but input can't be taken as a predefined variable. It needs to be taken through an allowed input format. \$\endgroup\$
    – lyxal
    Dec 27, 2022 at 10:34
  • \$\begingroup\$ (p:=[10,4][k]if(k:=f[1]^255)<2else 2) -> (p:={255:10,254:4}.get(f[1],2)) \$\endgroup\$
    – tsh
    Dec 28, 2022 at 6:07
  • \$\begingroup\$ bytes(x^q[i%4]for i,x in enumerate(f[p+4:])) -> bytes(x^m for x,m in zip(f[p+4:],q*len(f))) \$\endgroup\$
    – tsh
    Dec 28, 2022 at 6:09
  • \$\begingroup\$ the q*len(f) is really smart. \$\endgroup\$
    – asdf256
    Dec 28, 2022 at 8:40
  • \$\begingroup\$ f[(p:={255:10,254:4}.get(f[1],2)):][:4] -> f[(p:=([2]*254+[4,10])[f[1]]):p+4] \$\endgroup\$ Dec 28, 2022 at 15:07
3
\$\begingroup\$

Raku, 64 60 bytes

{Buf(.[^4] «+^«.[4..*]).decode}o{.[(2 max.[1]*6-1520)..*]}

Try it online!

(I discovered that I could shave off four bytes by calling Buf like a function to create a new buffer object, but the increasingly ancient version of Perl 6/Raku on TIO doesn't allow this syntax, so the link is to a previous version of my code that uses .new.)

This is two anonymous functions, composed together using the o operator.

The right brace-delimited expression is the first function to be applied, to an input list of integers, assumed to be in the range 0-255. It returns the tail of that list, starting with the xor key at the appropriate index given by the byte at index 1, .[1]. .[1] * 6 - 1520 gives 10 if 255 is at that index, 4 if 254 is at that index, and some number less than zero for any other byte. 2 max makes that number 2 if it's less than 2, giving the correct index for all cases.

The left brace-delimited function takes that key-plus-payload list of bytes and formats them into the output string. Buf(...).decode takes the list of bytes as the argument to construct a Buf object and decodes the utf-8 bytes into a regular string. The argument to Buf is .[^4] «+^« .[4..*]. .[^4] is the first four bytes (the xor key), and .[4..*] is rest of the bytes (the payload). +^ is the bitwise-xor operator, and the guillemets that surround it make it a "hyperoperator" with very useful semantics. The elements of both lists are xor-ed in sequence, and since the guillemets are pointing leftwards at the list of key bytes, those bytes are cyclically re-read as many times as needed to match the number of payload bytes. An equivalent construction would be to reverse both the order of the lists and the direction of the guillemets: .[4..*] »+^» .[^4].

\$\endgroup\$
3
\$\begingroup\$

Java 19, 214 bytes

-45, see @Kevin Cruijseen's comments. Thanks!

interface A{static void main(String[]a){var f=java.util.HexFormat.of().parseHex(a[0]);int l=f.length,g=f[1]&255,p=g>254?10:g>253?4:2,x=p+4,i=x;while(i<l)f[i]^=f[p+(i++-x)%4];System.out.print(new String(f,x,l-x));}}

Java 19, 259 bytes

-1 for space between String[] and a. Thanks!

interface A{static void main(String[]a){var f=java.util.HexFormat.of().parseHex(a[0]);var p=2;if((f[p]&0xff)==254)p+=2;if((f[p]&0xff)==255)p+=8;var x=p+4;for(var i=x;i<f.length;i++)f[i]=(byte)(f[i]^f[p+(i-x)%4]);System.out.print(new String(f,x,f.length-x));}}

Java 19, 260 bytes

interface A{static void main(String[] a){var f=java.util.HexFormat.of().parseHex(a[0]);var p=2;if((f[p]&0xff)==254)p+=2;if((f[p]&0xff)==255)p+=8;var x=p+4;for(var i=x;i<f.length;i++)f[i]=(byte)(f[i]^f[p+(i-x)%4]);System.out.print(new String(f,x,f.length-x));}}
\$\endgroup\$
6
  • \$\begingroup\$ You don't need the space between String[] and a. \$\endgroup\$ Dec 27, 2022 at 20:32
  • \$\begingroup\$ Both 0xff can be 255 for -2; ==255 can be >254 for -1; you can create temp variables for f[p]&255 and f.length to save some bytes; all var including the p if-statements can be combined to a single-line int; f[i]=(byte)(f[i]^f[p+(i-x)%4]); can be f[i]^=f[p+(i-x)%4]; i++ can be moved; and (lambda) functions are allowed by default, so no need for a full program. :) All in total: h->{var f=java.util.HexFormat.of().parseHex(h);int l=f.length,t=f[2]%255,p=t>254?10:t>253?4:2,x=4+p,i=x;for(;i<l;)f[i]^=f[p+(i++-x)%4];return new String(f,x,l-x);} (163 bytes) \$\endgroup\$ Dec 28, 2022 at 12:39
  • 1
    \$\begingroup\$ Here an ATO link (which is Java version 18) (your code seems to use require 17+) to see it in action. Also, welcome to CGCC! :) If you haven't seen it yet, tips for golfing in Java and tips for golfing in <all languages> might be interesting to read through. Enjoy your stay, and Merry Christmas! :) \$\endgroup\$ Dec 28, 2022 at 12:43
  • \$\begingroup\$ Oops, I've accidentally changed the &0xff to %255 instead of &255 in my comment/link above. Also, your f[p] - and therefore also my f[2] - should be f[1] instead. Otherwise the final test case gives incorrect output. \$\endgroup\$ Dec 28, 2022 at 12:49
  • 1
    \$\begingroup\$ Suggest 2<<t/254 instead of t>253?4:2 \$\endgroup\$
    – ceilingcat
    Dec 29, 2022 at 9:29
1
\$\begingroup\$

05AB1E, 22 21 (or 20?) bytes

¦¬₅<.S>3sm.$D4£Þ^4.$ç

Input as a list of integers; output as a list of characters.

Try it online or verify all test cases.

Explanation:

¦           # Remove the first value of the (implicit) input
 ¬          # Push its new first value (without popping the headless list)
  ₅<        # Push 254 (255 decreased by 1)
    .S      # Compare:                  -1 if <254; 0 if ==254; 1 if >254
      >     # Increase it by 1:          0 if <254; 1 if ==254; 2 if >254
       3sm  # Take 3 to the power that:  1 if <254; 3 if ==254; 9 if >254
.$          # Remove that many leading items from the headless list
  D         # Duplicate the remaining list
   4£       # Pop the copy, leave just its first 4 values
     Þ      # Cycle this list indefinitely
      ^     # Bitwise-XOR the values at the same positions in the lists together
            # (ignoring trailing items of the infinite list)
       4.$  # Remove the four leading 0s †
ç           # Convert every integer to a character with that codepoint
            # (after which this list of characters is output implicitly)

4.$ could be golfed to (trim all leading 0s), if the encrypted text is guaranteed to never start with a leading ␀-byte.

\$\endgroup\$
1
  • \$\begingroup\$ Payloads are only guaranteed to be valid UTF-8. Unfortunately a sequence of \00 is valid. \$\endgroup\$
    – sheared
    Dec 28, 2022 at 15:28
1
\$\begingroup\$

C (GCC), 120 bytes

#define g getchar()
x[4];i;n;y;main(){g;for((y=g)>253&&(n=y-255?2:8);n+4;)x[n--&3]=g;for(;(n=g)+1;)putchar(n^x[i--&3]);}

Attempt This Online!

\$\endgroup\$
4
  • \$\begingroup\$ I'm pretty sure this fails for test case 1 because byte offsets aren't handled properly. \$\endgroup\$
    – sheared
    Dec 28, 2022 at 15:20
  • \$\begingroup\$ @sheared I misread the challenge, now it's fixed \$\endgroup\$
    – matteo_c
    Dec 30, 2022 at 16:53
  • 1
    \$\begingroup\$ 117 bytes \$\endgroup\$
    – ceilingcat
    Dec 31, 2022 at 18:11
  • \$\begingroup\$ 106 bytes w/ main->function \$\endgroup\$
    – att
    Jan 6 at 8:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.