Skip to content

Instantly share code, notes, and snippets.

@Nullreff
Last active December 20, 2015 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nullreff/6151701 to your computer and use it in GitHub Desktop.
Save Nullreff/6151701 to your computer and use it in GitHub Desktop.
// Called whenever you press "Reload Comic"
function reload_comic(){fetch_and_set("http://prizes.prequeladventure.com/wlf2/qwtake-control-data.json", '#comic', "#votes", ".comicadjustment");}
function fetch_and_set(url, target, votes, domadjust ) {
jQuery.getJSON(url, function (data) {
/* Data is:
* f: Url with a bunch of data, seems to always be:
* "http://prizes.prequeladventure.com/wlf2/qwtake-control-crypt.txt"
*
* Passed into the 'pd' function along with the data taken for 'f' url:
* i: IV used to decode: https://code.google.com/p/crypto-js/#Custom_Key_and_IV
* k: decryption key
* l: Number of parts to remove from the start
*
* Uninteresting:
* msg: Not used
* v: Number of votes so far
*
*
*/
jQuery.get(data.f, function (ct) {
// Part that displays the comic
jQuery(target).html(pd(ct, data.l, data.k, data.i));
// And a bunch of other crap we don't care about
if ( votes ) { jQuery(votes).text(data.v); }
if ( domadjust ) {
jQuery(domadjust).filter('.notcomplete').toggle(data.l>0).end().filter('.complete').toggle(data.l<=0);
}
});
});
}
// This does all the decryption
function pd(data, start, key, iv) {
// https://code.google.com/p/crypto-js/
var C=CryptoJS, cts=[];
iv = C.enc.Hex.parse(iv);
key = C.enc.Base64.parse(key);
// The comic is stored as Base64, AES encrypted chunks taken from
// http://prizes.prequeladventure.com/wlf2/qwtake-control-crypt.txt
// when it's pulled in, all the parts are split up then all the ones
// we haven't "unlocked" yet are removed from the start
jQuery.each(data.split(/\r?\n/).slice(start), function (i, v) {
// So it decodes the first one using the key (which changes every
// time new stuff is unlocked) and IV (which appears to be constant).
var ct = C.AES.decrypt(
C.lib.CipherParams.create({ciphertext: C.enc.Base64.parse(v)}),
key,
{ iv: iv }
).toString(C.enc.Latin1);
// Save it to the array (comic data comes in reverse order).
cts.unshift(ct);
// TRICKY, generates a new key using the stuff it just decrypted
// so each part has a different decryption key that is generated
// using the previous part and its decryption key. So you only
// have to send one key and the rest of the comic decrypts itself
key = C.SHA1(ct + key);
});
return cts.join("");
}
// So in order to unlock the entire comic, you need the key for the first part
// and then set 'start' to 0. Very simple but requires knowing the key.
//
// If anyone can brute force the key for "XUkMfuNkgNnR2KWckpPDTA=="(base64),
// the whole comic is available. Although by the time you do, it's probably
// already going to have enough votes...
@chmarr
Copy link

chmarr commented Aug 4, 2013

Perfect explanation! (I must have mis-read it before and only deemed it near-perfect).

You saved me documentation work, too! Thank you!

@Nullreff
Copy link
Author

Nullreff commented Aug 4, 2013

Thanks for the code, I had a fun time figuring it all out.

@chmarr
Copy link

chmarr commented Aug 4, 2013

You're welcome!

And thanks for highlighting the "TRICKY" part. That, plus the fact the comic is chain-block-encrypted in reverse order, is the key (pun!) to this novel (I think) use of encryption. I actually wanted to avoid splitting the comic up into chunks, but I could not figure out how to securely emit the internal state of a decryptor so one can decrypt starting at a certain point, without also then having the information to decrypt from the start. Due to lack of time, I settled on this method.

I'll release the "encryption" side — which is not much more than the reverse of the above — and the server-side code after the contest closes.

@Nullreff
Copy link
Author

Nullreff commented Aug 4, 2013

Due to lack of time, I settled on this method.

It's definitely "good enough". However, it does provide an easy verification method if someone were to try and brute force out the key. Then again, trying to brute force AES with a non dictionary key is bad idea. I look forwards to seeing the server side code once the contest is over.

@chmarr
Copy link

chmarr commented Aug 4, 2013

However, it does provide an easy verification method if someone were to try and brute force out the key.

Not any more than plain crypto. Each "chunk" is only 5 bytes, and the key is 20 bytes (with only 16 being used). So there are 2^88 possible keys that will generate the correct output for that chunk, and that is assuming you know exactly what is in that chunk. So, even if you get the right output (which you can't be sure of), only 2^48 of those keys would be valid to decrypt the next, and so on.

The numbers are probably off given that some extra verification is possible due to the padding scheme. But in no case does this make the scheme less "verifiable" than encryption of a large file.

@Nullreff
Copy link
Author

Nullreff commented Aug 5, 2013

So, even if you get the right output (which you can't be sure of)

You don't have to know if you got the correct output. Taking the SHA1 of the decrypted data + key gives you the key for the next "chunk". You just keep decrypting until you reach a key that's already been released. Would look something like this:

latest_index = 2587
latest_key = "rUAduksKEJV3L1SfF6N+w00K2NI="
data = .....

// Given the above information about a key we already know
// this verifies keys at a specific index
def verify(key, index):
    // If we've hit the last released key, verify it
    if (index == latest_index):
        return (key == latest_key)

    // Otherwise compute the next key down
    decrypted = decrypt(data[index], key)
    new_key = SHA1(decrypted + key)

    // And recurse
    verify(new_key, index + 1)


// Do the actual searching
While True:
    key = get_some_key_to_check()
    if (verify(key, 0)):
        print("Found it: %s".format(key))

Including that trick with the SHA1 means that once someone guesses the first key, they can easily verify based on the previously released keys and decrypt the rest of the comic. One interesting property is that the more of the comic you release, the faster the verification algorithm gets. Even with this trick, its still completely unfeasible to guess the key...

@chmarr
Copy link

chmarr commented Aug 5, 2013

Still wouldn't work, since you can't be sure you arrived at the correct key because you happened to get the right key/content combination, or happened to find some other combination that worked.

And even if you exclude that, you still have a huge space to iterate over. So... this is no "less secure" than standard file crypto.

@Nullreff
Copy link
Author

Nullreff commented Aug 5, 2013

Still wouldn't work, since you can't be sure you arrived at the correct key because you happened to get the right key/content combination, or happened to find some other combination that worked.

No one has been able to produce a SHA1 collision. Even so, it would narrow the potential answer space down to the point where you could manually verify each result.

And even if you exclude that, you still have a huge space to iterate over. So... this is no "less secure" than standard file crypto.

True, I think I'm just arguing for the sake of arguing now...

@chmarr
Copy link

chmarr commented Aug 5, 2013

No one has been able to produce a SHA1 collision. Even so, it would narrow the potential answer space down to the point where you could manually verify each result.

With a larger file you can do that anyway: If after 2 or 3 blocks the decrypted result is still in the ASCII range, you effectively confirmed you have the right key (or, technically, a key/iv combination).

True, I think I'm just arguing for the sake of arguing now...

What? That never happens on the internet! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment