views:

288

answers:

4

OK, I'm probably just having a bad Monday, but I have the following need and I'm seeing lots of partial solutions but I'm sure I'm not the first person to need this, so I'm wondering if I'm missing the obvious.

$client has 50 to 500 bytes worth of binary data that must be inserted into the middle of a URL and roundtrip to their customer's browser. Since it's part of the URL, we're up against the 1K "theoretical" limit of a GET URL. Also, $client doesn't want their customer decoding the data, or tampering with it without detection. $client would also prefer not to store anything server-side, so this must be completely standalone. Must be Perl code, and fast, in both encoding and decoding.

I think the last step can be base64. But what are the steps for encryption and hashing that make the most sense?

A: 

How secure does it need to be? Could you just xor the data with a long random string then add an MD5 hash of the whole lot with another secret salt to detect tampering?

I wouldn't use that for banking data, but it'd probably be fine for most web things...

big

bigiain
Oddly enough, that's sorta what they're doing already, except that (a) the string is too short and (b) the customer can control some of the data, so known-plaintext attacks are possible. Since I'm breaking backward compatibility, I wanted to do something that wouldn't end up in thedailywtf.com, which the current code is clearly worthy of.
Randal Schwartz
Fair enough, I'm sitting here thinking up ways to "complexify" that approach, but if you want "proper" security you probably ought to be talking to someone who's a proper crypto geek. (I'm only just smart enough to know I'll inevitably get crypto wrong by myself...)
bigiain
With CPAN it's easier to not "complexify" that approach and just use a CPAN module to encrypt the data using something known to be secure.
perigrin
+4  A: 

Create a secret key and store it on the server. If there are multiple servers and requests aren't guaranteed to come back to the same server; you'll need to use the same key on every server. This key should be rotated periodically.

If you encrypt the data in CBC (Cipher Block Chaining) mode (See the Crypt::CBC module), the overhead of encryption is at most two blocks (one for the IV and one for padding). 128 bit (i.e. 16 byte) blocks are common, but not universal. I recommend using AES (aka Rijndael) as the block cipher.

You need to authenticate the data to ensure it hasn't been modified. Depending on the security of the application, just hashing the message and including the hash in the plaintext that you encrypt may be good enough. This depends on attackers being unable to change the hash to match the message without knowing the symmetric encryption key. If you're using 128-bit keys for the cipher, use a 256-bit hash like SHA-256 (you can use the Digest module for this). You may also want to include some other things like a timestamp in the data to prevent the request from being repeated multiple times.

edanite
Is it necessary to authenticate the data? How can the client send modified data without knowing the key?
Schwern
@Schwern, Randal mentions in another comment that the client can provide some of the data included in the data to be encrypted. A clever attacker might be able to use that to alter the data without really knowing the key. At one point StackOverflow was vulnerable to this attack, Jeff and Joel discussed the details in one of the early podcasts.
Ven'Tatsu
@Ven'Tatsu Do you have a link to those podcasts? I'll admit I'm no data security expert and would be interested to learn about this.
Schwern
+4  A: 

I have some code in a Cat App that uses Crypt::Util to encode/decode a user's email address for an email verification link.

I set up a Crypt::Util model using Catalyst::Model::Adaptor with a secret key. Then in my Controller I have the following logic on the sending side:

my $cu = $c->model('CryptUtil');
my $token = $cu->encode_string_uri_base64( $cu->encode_string( $user->email ) );
my $url = $c->uri_for( $self->action_for('verify'), $token );

I send this link to the $user->email and when it is clicked on I use the following.

my $cu = $c->model('CryptUtil');
if ( my $id = $cu->decode_string( $cu->decode_string_uri_base64($token) ) ) {
    # handle valid link
} else { 
    # invalid link
}

This is basically what edanite just suggested in another answer. You'll just need to make sure whatever data you use to form the token with that the final $url doesn't exceed your arbitrary limit.

perigrin
FWIW this means that someone could capture a specific instance of this, can replay it (i.e. use it again). It's up to the OP, I suppose, to decide what sort of risk that poses.
Noon Silk
As others have pointed out though you could trivially add a datetime/counter/"one time pad" to the message to check for that. Randall didn't explain his use case well enough to fully vet what he wanted, and he's a smart guy I'm sure he'll figure it out. ;)
perigrin
+2  A: 

I see three steps here. First, try compressing the data. With so little data bzip2 might save you maybe 5-20%. I'd throw in a guard to make sure it doesn't make the data larger. This step may not be worth while.

use Compress::Bzip2 qw(:utilities);
$data = memBzip $data;

You could also try reducing the length of any keys and values in the data manually. For example, first_name could be reduced to fname.

Second, encrypt it. Pick your favorite cipher and use Crypt::CBC. Here I use Rijndael because its good enough for the NSA. You'll want to do benchmarking to find the best balance between performance and security.

use Crypt::CBC;
my $key = "SUPER SEKRET";
my $cipher = Crypt::CBC->new($key, 'Rijndael');
my $encrypted_data = $cipher->encrypt($data);

You'll have to store the key on the server. Putting it in a protected file should be sufficient, securing that file is left as an exercise. When you say you can't store anything on the server I presume this doesn't include the key.

Finally, Base 64 encode it. I would use the modified URL-safe base 64 which uses - and _ instead of + and / saving you from having to spend space URL encoding these characters in the base 64 string. MIME::Base64::URLSafe covers that.

use MIME::Base64::URLSafe;
my $safe_data = urlsafe_b64encode($encrypted_data);

Then stick it onto the URL however you want. Reverse the process for reading it in.

You should be safe on size. Encrypting will increase the size of the data, but probably by less than 25%. Base 64 will increase the size of the data by a third (encoding as 2^6 instead of 2^8). This should leave encoding 500 bytes comfortably inside 1K.

Schwern
Uhh tamper protection? Sorry but if I know the message somehow I can replace it undetected with this scheme. The simplest tamper protection is to include in the encrypted container the hash of the message + secret nonce known only to the server.
Joshua
How do you replace it without knowing the key? Or do you mean one could take a different encrypted piece of data and slot it in?
Schwern