views:

326

answers:

5

I'd like to keep my database clean of stale almost-accounts, and I was thinking about making new signups and invitations put their data into the welcome email as an encrypted or hashed url. Once the link in the url is visited, the information is then added into the database as an account. Is there something that currently does this? Any references, thoughts, or warnings about doing user registration this way? Thanks!

Edit: I've made a working example, and the url is 127 characters.

http://localhost/confirm?_=hBRCGVqie5PetQhjiagq9F6kmi7luVxpcpEYMWaxrtSHIPA3rF0Hufy6EgiH%0A%2BL3t9dcgV9es9Zywkl4F1lcMyA%3D%3D%0A

Obviously, more data = larger url

def create
# Write k keys in params[:user] as v keys in to_encrypt, doing this saves LOTS of unnecessary chars
  @to_encrypt = Hash.new
  {:firstname => :fn,:lastname => :ln,:email => :el,:username => :un,:password => :pd}.each do |k,v|
    @to_encrypt[v] = params[:user][k]
  end

  encrypted_params = CGI::escape(Base64.encode64(encrypt(compress(Marshal.dump(@to_encrypt)), "secret")))
end

private

def aes(m,t,k)
  (aes = OpenSSL::Cipher::Cipher.new('aes-256-cbc').send(m)).key = Digest::SHA256.digest(k)
  aes.update(t) << aes.final
end

def encrypt(text, key)
  aes(:encrypt, text, key)
end

def decrypt(text, key)
  aes(:decrypt, text, key)
end

# All attempts to compress returned a longer url (Bypassed by return)

def compress(string)
  return string
  z = Zlib::Deflate.new(Zlib::BEST_COMPRESSION)
  o = z.deflate(string,Zlib::FINISH)
  z.close
  o
end

def decompress(string)
  return string
  z = Zlib::Inflate.new
  o = z.inflate(string)
  z.finish
  z.close
  o
end
+4  A: 

Thoughts:

  • Use true asymmetric cypher for the "cookie" to prevent bots creating accounts. Encrypt the "cookie" using public key, verify it by decoding with private key.
    Rationale: If only a base64 or other algorithm was used for encoding the cookie, it would be easy to reverse-engineer the scheme and create accounts automatically. This is undesirable because of spambots. Also, if the account is password protected, the password would have to appear in the cookie. Anyone with access to the registration link would be able not only to activate the account, but also to figure out the password.

  • Require re-entry of the password after activation through the link.
    Rationale: Depending on the purpose of the site you may want to improve the protection against information spoofing. Re-entering the password after activation protects against stolen/spoofed activation links.

  • When verifying the activation link, make sure the account created by it is not created already.

  • How do you protect against two users simultaneously creating an account with the same name?
    Possible answer: Use email as the login identifier and don't require unique account name.

  • Verify the email first, than continue account creation.
    Rationale: This will minimize the information you need to send in the cookie.

Filip Navara
+4  A: 
  • There are some e-mail clients which break URLs after 80 letters. I doubt that you can fit all the information in there.

  • Some browsers have limitations for the URL, Internet Explorer 8 has a limit of 2083 characters, for example.

Why don't you clean your database regularly (cron script) and remove all accounts that haven't been activated for 24 houres?

Georg
Hmm, these limits are something that will probably make it hard to use any encryption algorithm like RSA...
Filip Navara
The goal is to keep as much temporary and junk data out of the database. I'd rather not be surprised by some uber-smart robot ravaging my signup process and adding zillions of rows of data.Plus the various stages of signups/invitations require a reasonable amount of effort to build and maintain. Obviously something like OpenID is an option, but not everyone has that.
Blaine
I don't see 2083 characters being a big problem for small amounts of data. This method could also prevent ridiculous accounts (Angry/Dumb Human, Spambot) in which the username/password is 1000 characters.Plain text email has a limitation of 78 (recommended) to 998 characters per line (See Section 2.1.1)http://www.ietf.org/rfc/rfc2822.txt
Blaine
Which email clients break URLs after 80 letters? Are you describing an 80 character line break that is snapping a URL?
Mike Buckbee
@Mike: Some versions of Apples Mail: http://www.macosxhints.com/article.php?story=20070112124954101
Georg
I vote for cleaning the database regularly. If you are worried about performance, use a separate table for unverified accounts. You can put in other checks if you are worried about spam. No matter what way you implement user registration, you are going to have to worry about spam bots, so you might as well keep it simple.
Jon Snyder
+1  A: 

I will take a crack at describing a design that may work.

Prerequisities:

  • Cryptography library with support for RSA and some secure hash function H (eg. SHA-1)
  • One pair of private and public keys

Design:

  • Unique user identifier is e-mail address
  • An account has associated password and possible other data
  • The activation cookie is kept as small as possible

Process:

  • User is asked for e-mail address and password. Upon submission of the form a cookie is computed as cookie = ENCRYPT(CONCAT(email, '.', H(password)), public key)
  • E-mail is sent containing a link to the activation page with the cookie, eg. http://example.org/activation?cookie=[cookie]
  • The activation page at http://example.org/activation decrypts the cookie passed as parameter: data = SPLIT(DECRYPT(cookie, private key), '.')
  • In the same activation page the user is asked for password (which must be hashed to the the same value as in cookie) and any other information necessary for the account creation
  • Upon submission of the activation page a new account is created

Please point out anything that I have missed or any improvements. I'd be glad to update the answer accordingly.

Filip Navara
Check my code sample http://gist.github.com/181566, using aes256, I'm using a URL param instead of a cookie. It follows the basic principles you outlined, but I shortened it. Thanks!
Blaine
Yeah, I meant url parameter, the "cookie" wasn't meant to be a HTTP cookie. You used AES, which seems perfectly fine for this purpose. It didn't occur to me earlier that symmetric cypher may work here. Note that using username introduces the problem with two users choosing the same username and not being notified about it until they both try to activate. I'd strongly suggest to use logins via email address, if possible.
Filip Navara
For this particular instance I'll be sticking with username, as there's a relatively low number of users. I do see your point, and I agree that emails will be more reliable for larger sites with more users.
Blaine
+3  A: 

I have done pretty much the same before. I only have 2 suggestions for you,

  1. Add a key version so you can rotate the key without breaking outstanding confirmation.
  2. You need a timestamp or expiration so you can set a time limit on confirmation if you want to. We allow one week.

As to the shortest URL, you can do better by making following changes,

  1. Use a stream cipher mode like CFB so you don't have to pad to the block size.
  2. Compress the cleartext will help when the data is big. I have a flag and only use compression when it shrinks data.
  3. Use Base64.urlsafe_encode64() so you don't have to URL encode it.
ZZ Coder
Thanks for your help! CFB definitely makes more sense!
Blaine
+3  A: 

There's a few problems with your solution.

Firstly, you're not setting the IV of the cipher. In my view this has exposed a serious bug in the Ruby OpenSSL wrapper - it shouldn't let you perform an encryption or decryption until both key and iv have been set, but instead it's going ahead and using an IV of all-zeroes. Using the same IV every time basically removes much of the benefit of using a feedback mode in the first place.

Secondly, and more seriously, you have no authenticity checking. One of the properties of CBC mode is that an attacker who has access to one message can modify it to create a second message where a block in the second message has entirely attacker-controlled contents, at the cost of the prior block being completely garbled. (Oh, and note that CFB mode is just as much a problem in this regard).

In this case, that means that I could request an account with Last Name of AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA and my own email address to recieve a valid URL. I can then, without knowing the key, modify the email address to [email protected] (and garble the Last Name in the process, which doesn't matter), and have a valid URL which I can submit to your server and create accounts for email addresses that I don't control.

To solve this, you need to compute a HMAC over the data, keyed with a secret that only the server knows, and send that as part of the URL. Note that the only reason you need encryption at all here is to secure the user's password - other than that it could just be plaintext plus a HMAC. I suggest you simply send as the url something like:

?ln=Last%20Name&fn=First%20Name&[email protected]&hmac=7fpsQba2GMepELxilVUEfwl3%2BN1MdCsg%2FZ59dDd63QE%3D

...and have the verification page prompt for a password (there doesn't seem to be a reason to bounce the password back and forth).

caf
Thanks for taking the time to write a response! I've read it and I agree with what you're saying with regards to prompting for a password upon verification. I'll probably keep the encryption in place, and use an IV along with a sha1 or md5 hash inside the encrypted params, that way the request can't be forged. Though, in my experience testing the current vulnerable technique by modifying the url made the decryption process fail. I'll have to find out if aes256 cfb has its own consistency check.
Blaine
To modify it you would have to be careful to not to disrupt the Ruby object marshalling delimiters, but it's certainly possible. It's up to you of course, but I strongly recommend using the right tool for the right job (Ruby provides a HMAC function, with `OpenSSL::HMAC` that seems pretty good).
caf
Ok, I didn't realize Ruby has an HMAC function. In my experience OpenSSL has really bad documentation, and hard to find anything without knowing what you're looking for. Thanks!
Blaine