11

My ultimate goal is to create a URL that is unique and cannot be guessed/predicted. The purpose of this URL is to allow users to perform operations like verifying their email address and resetting their password. These URLs would expire within a set amount of time (currently set to 24 hours).

I was originally using a Guid for this purpose, but I now understand this to be somewhere between "just fine" and "very insecure", depending on which expert you listen to. So, I thought I'd beef up my code a little bit, just in case. At first I thought I'd just stick with using a Guid, but generate it from random bytes rather than the Guid.NewGuid() factory method. Here is the method I came up with:

public static Guid GetRandomGuid()
{
    var bytes = new byte[16];
    var generator = new RNGCryptoServiceProvider();
    generator.GetBytes(bytes);
    return new Guid(bytes);
}

I'm not quite clear on what exactly happens when you use new Guid(bytes) instead of Guid.NewGuid(), but I think all this method does is generate a random byte array and store it in a Guid data structure. In other words, it's no longer guaranteed to be unique, it's only guaranteed to be random.

Since my URLs need to be both unique and random, this does not seem like a solid solution. I'm now thinking, I should base my URLs on a combination of both a unique ID (which could be a Guid or, if available, a database auto-incremented id) and a random sequence generated from RNGCryptoServiceProvider.

Questions

What's the best way to generate a verification/password-reset URL that is both guaranteed unique and extremely difficult/impossible to predict/guess?

  • Should I simply construct my URL by concatenating a unique string with a random string?

  • Does the .NET Framework have a built-in way to easily generate a unique and random (unpredictable) sequence that can be used for URLs?

  • If not, is there a good solution available open source?

Update

In case anyone has a similar requirement, I'm currently using the following method:

public static string GenerateUniqueRandomToken(int uniqueId)
    // generates a unique, random, and alphanumeric token
{
    const string availableChars =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    using (var generator = new RNGCryptoServiceProvider())
    {
        var bytes = new byte[16];
        generator.GetBytes(bytes);
        var chars = bytes
            .Select(b => availableChars[b % availableChars.Length]);
        var token = new string(chars.ToArray());
        return uniqueId + token;
    }
}

Please comment if you see any problems with this.

devuxer
  • 41,681
  • 47
  • 180
  • 292
  • Does [this](http://stackoverflow.com/a/817150/116923) answer help you at all? – adrianbanks Sep 03 '12 at 01:00
  • I would suspect this is going to be your best bet. It obviously won't conform to the GUID standards. – Alastair Pitts Sep 03 '12 at 01:01
  • It looks like you've simply created a 16-byte random number, not a GUID. – Gabe Sep 03 '12 at 01:17
  • Seems like simply generating a guid then using the hash of that guid would give you what you need (uniqueness and randomness) – Eric Petroelje Dec 10 '12 at 19:46
  • @EricPetroelje, guids are not random, so why would a hash of a guid be random? – devuxer Dec 10 '12 at 20:08
  • @DanM - because part of the definition of a good hash is that even a small change in the input value should result in a large change in the hash value. So even if the guids were sequential, the hashes would vary significantly from one to the next. – Eric Petroelje Dec 10 '12 at 20:09
  • @DanM - I suppose randomness is not strictly what you would be getting here. Moreso that having one hash gives you no information about what the next (or any other) hash will be. Which I'm thinking is what you are actually looking for – Eric Petroelje Dec 10 '12 at 20:13
  • @EricPetroelje, good points. What I want is an "unguessable" URL. I think hashing does accomplish that. I imagine I could just use BCrypt to create the hash, then it would also be salted, which would make the possibility of guessing even more remote. – devuxer Dec 10 '12 at 20:21
  • @EricPetroelje, thinking about this some more, the problem with hashes is that they are *not* guaranteed to be unique, even if the guids they are generated from are unique. So, I believe I'd still need to append my database-generated unique ID to the hash to guarantee uniqueness. And if I do that, I'm not sure there's much benefit over my current method. – devuxer Dec 10 '12 at 20:36
  • @DanM - true, hashes are not guaranteed to be unique, but statistically speaking, the chances of getting a duplicate hash value are extremely low. Like one-in-a-million-if-you-generated-a-million-hashes-per-second-from-now-until-the-sun-burns-out low :) – Eric Petroelje Dec 10 '12 at 20:39
  • @EricPetroelje Gotcha, I'll give it some more thought. Thanks for the feedback! – devuxer Dec 10 '12 at 20:52

5 Answers5

6

A Guid is not meant to be a security feature. The standards for creating a Guid make no claims about how secure it is, or how easy/hard it is to guess. It simply makes a claim about it's uniqueness.

If you attempt to secure your system by using a Guid then you will not have a secure system.

Community
  • 1
  • 1
Josh
  • 44,706
  • 7
  • 102
  • 124
  • My code used to be just `Guid.NewGuid()`. I wrote the above method in at attempt to improve security. What I was *trying* to do with this method was use a cryptographic random number generator to generate the bytes and then simply store those bytes in a `Guid` data structure (for purposes of database storage). Is this not what my code does? And if it's not, what do you recommend? I use `Guid`s for generating unique URL's for password resets, etc. All of these URL's expire 24 hours after they are issued. – devuxer Sep 03 '12 at 03:09
  • I think the problem is that a GUID is inherently predictable because it follows specific rules. Why not just create a random cryptographic hash based on your own set of rules. You could base it on the username and other information specific to your server setup... that would be basically impossible to guess without having intimate knowledge of your server. A GUID is specific, the rules are known, so therefore it is theoretically guessable, although mathematically improbable. – Josh Sep 03 '12 at 03:32
  • 1
    The problem with a hash is that it's not guaranteed to be unique. I'm pretty sure what I've done here is not guaranteed to be unique either. But I need both unique and random. – devuxer Sep 03 '12 at 04:36
0

[Edit: I missed the call to RNGCryptoServiceProvider above. Apologies about that.]

The problem with RNG generators like the RNGCryptoServiceProvider for your case is they do not guarantee uniqueness. Guids, as you know, are statistically unlikely to be unique but do not guarantee it. The only real way to guarantee both uniqueness and randomness is to generate the GUID like you have then put it in a searchable store, like a database table. Whenever you generate a new value, check it is not already present. If it is, discard it and generate a new value.

akton
  • 14,148
  • 3
  • 43
  • 47
  • Please take a closer look at my code. I'm already using RNGCrptoServiceProvider. – devuxer Sep 03 '12 at 04:28
  • @DanM My apologies. I have rewritten my answer to fix that. – akton Sep 03 '12 at 04:53
  • I remember reading that blog post by Eric Lippert. It sounds like if all `Guid`s are generated on the same machine (as they will be in the case of this particular app), uniqueness really is guaranteed. That said, a database assigned auto-incrementing Id would also be guaranteed unique, and it looks like I have access to one in each case I'm currently using a Guid. So, I *could* combine this unique Id with a 128-bit crypto-strength random number and store the whole thing as a string in the database. Do you think that would suffice? – devuxer Sep 03 '12 at 07:02
0

You can generate 128 bit, "random," unique numbers by running a counter through an AES counter keyed with a random key. As long as the same key is used this will never repeat any output.

static byte[] AESCounter(byte[] key, ulong counter) {
    byte[] InputBlock = new byte[16];
    InputBlock[0] = (byte)(counter & 0xffL);
    InputBlock[1] = (byte)((counter & 0xff00L) >> 8);
    InputBlock[2] = (byte)((counter & 0xff0000L) >> 16);
    InputBlock[3] = (byte)((counter & 0xff000000L) >> 24);
    InputBlock[4] = (byte)((counter & 0xff00000000L) >> 32);
    InputBlock[5] = (byte)((counter & 0xff0000000000L) >> 40);
    InputBlock[6] = (byte)((counter & 0xff000000000000L) >> 48);
    InputBlock[7] = (byte)((counter & 0xff00000000000000L) >> 54);
    using (AesCryptoServiceProvider AES = new AesCryptoServiceProvider())
    {
        AES.Key = key;
        AES.Mode = CipherMode.ECB;
        AES.Padding = PaddingMode.None;
        using (ICryptoTransform Encryptor = AES.CreateEncryptor())
        {
            return Encryptor.TransformFinalBlock(InputBlock, 0, 16);
        }
    }
}
0

Get md5 of of all the user login credentials and the concatenate it with the guid generated by Guid.NewGuid().ToString() and use it in your url, it should work fine.

Sam
  • 925
  • 1
  • 12
  • 28
0

Create a table in the database with a linkID and a Datesent column, on generation of the link send insert DateTime.Now into the table and return linkId, set the linkID as a querystring parameter on the activationLink.

On load of the activation page retrive the linkId and use it to evoke a stored procedure that will return the date when passed the corresponding linkId as a parameter, when you get the date back you can add how long you want the link to stay active by using the .AddDays()/.AddMonths (this are C# methods for datetime). Then compare the date you got back with today's date. If it has passed its length of days or months give an error message or else continue and display the page content.

I sugest you keep the contents of the page in a panel and and set it visible = "false" then only make the panel visible="true" if the date is still within range.

AJPerez
  • 3,435
  • 10
  • 61
  • 91
Spegah
  • 11
  • 3