10

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?

Randal Schwartz
  • 39,428
  • 4
  • 43
  • 70
  • You'll need to store something server-side. And I don't get what 'stand-alone' means in this case, how will it access the URL then? – Noon Silk May 24 '10 at 22:44
  • If I'm understanding this, you need a way to encrypt 50-500 bytes of data in a URL without blowing over 1k of text? – Schwern May 24 '10 at 22:55
  • I have to go from $data to customer URL back to $data. Nothing will be stored server side. – Randal Schwartz May 24 '10 at 22:55
  • @Schwern - yes, and also detect tampering, although the threat level is pretty low. – Randal Schwartz May 24 '10 at 22:56
  • If nothing is server side, why use a server? Also, is there any particular reason you can't POST the data? – pdehaan May 24 '10 at 23:15
  • Can't POST... ends up in strings, for example. – Randal Schwartz May 24 '10 at 23:21
  • I presume we can at least store a key on the server or in the code? – Schwern May 24 '10 at 23:31
  • Sounds like you really want Seaside in Perl. :) – brian d foy May 25 '10 at 04:53
  • 1
    @brian d foy: `Continuity` perhaps? http://search.cpan.org/dist/Continuity/ – draegtun May 25 '10 at 08:55
  • No, Continuity isn't what I'm thinking about. The handles the continuity bit with a session ID, but I was thinking about generating unique and secure links for every link that goes out. – brian d foy May 25 '10 at 12:02
  • @brian d foy: By default Continuity uses a cookie but this can be changed (see Continuity::Mapper) so creating and using unique/secure links should be possible. – draegtun May 27 '10 at 08:20
  • Yeah, but I can also create unique and secure links in another framework with the same amount of work. Continuity isn't special for that. – brian d foy May 27 '10 at 09:56
  • @brian d foy: Sounds like you don't really want Seaside in Perl then :) – draegtun May 27 '10 at 11:39
  • You know, it was really just a throwaway line because Randal is a Smalltalk guy. I think you missed the joke. Seaside and Continuity have a minor overlap in features, but that doesn't mean Continuity is the small thing as Seaside. – brian d foy May 27 '10 at 14:08
  • @brian d foy: No I got the joke as I avidly follow Randal's Smalltalk blog and have also (on a few occasions) played with Seaside. All the smileys were in the correct place! Barry – draegtun Jun 08 '10 at 11:52

4 Answers4

5

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
  • 4,433
  • 19
  • 22
  • 2
    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 May 25 '10 at 00:07
  • 1
    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 May 25 '10 at 00:22
4

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.

gmacon
  • 183
  • 9
  • Is it necessary to authenticate the data? How can the client send modified data without knowing the key? – Schwern May 24 '10 at 23:33
  • @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 May 25 '10 at 20:14
  • @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 May 25 '10 at 23:25
3

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
  • 153,029
  • 25
  • 195
  • 336
  • 1
    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 May 24 '10 at 23:37
  • 1
    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 May 25 '10 at 00:09
-1

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
  • 809
  • 5
  • 8
  • 3
    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 May 24 '10 at 22:59
  • 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 May 24 '10 at 23:07
  • 2
    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 May 25 '10 at 00:25