> The whole point of math/rand is that it's not cryptographically secure.
Sure, and that's even been in the documentation since before Go 1.0, yet people still make mistakes with it in practice. I've found it worth making the point explicit. Particularly in a case like this, where a casual reader might not notice the distinction between "non-reproducible" and "secure".
If you, as a developer who doesn't know much about random numbers or cryptography, think you need a random value, and you don't know if it needs to be cryptographically secure or not, you may as well just use a cryptographic RNG interface (unless you're using so much that performance or entropy becomes an issue.)
I think in most cases, it's pretty benign if you use cryptographic randomness even when it's not necessary. But, if you use math/rand when you wanted cryptographic randomness, to generate IDs or some such, that would be a much worse outcome.
Maybe it's bad for someone to use an RNG without understanding this well enough, though, and they should instead use a higher level abstraction if at all possible. But I can get behind the general idea.
> If you, as a developer who doesn't know much about random numbers or cryptography, think you need a random value, and you don't know if it needs to be cryptographically secure or not, you may as well just use a cryptographic RNG interface (unless you're using so much that performance or entropy becomes an issue.) I think in most cases, it's pretty benign if you use cryptographic randomness even when it's not necessary. But, if you use math/rand when you wanted cryptographic randomness, to generate IDs or some such, that would be a much worse outcome.
This is more or less what I was getting at. The main two downsides to using crypto/rand are:
- ergonomics (crypto/rand is a less user-friendly interface than math/rand)
- concurrent performance
The first one can be easily solved with a wrapper[0]. The second is particularly relevant here, because the main distinguishing feature of the global RNG in math/rand is that it is safe for concurrent use, whereas user-instantiated RNGs in math/rand are not. The big downside to this is that it's very easy to end up with performance issues due to mutex contention when multiple packages all use the global RNG (which is common in practice).
I actually submitted a CL (patch) to fix the mutex contention issue in the global RNG about five years ago, but it was rejected on the grounds that callers might depend on the specific sequence of numbers with the default seed, which would arguably break the compatibility promise. That apparently is no longer a concern (this change breaks the same thing, and the notes in the CL justify it), so I might resubmit it now.
crypto/rand is a little less performant in the single-threaded case, but not much - I think it'd be rare for that to be the bottleneck in real-life workloads at scale. The mutex, on the other hand, is a common bottleneck - I've run into this multiple times in multiple different codebases, one of which is what motivated the aforementioned CL.
So I generally advise people to use crypto/rand unless they are certain they need reproducibility, because the potential downside of accidentally using a non-secure RNG when you actually need one is quite high[1], but the downside of using a threadsafe cryptographically-secure one when you needed a threadsafe non-secure one is quite low: you're already taking much of the performance hit because of the mandated mutex, so the number of use cases that actually require the global RNG is quite small.
[1] there are a number of places where RNGs end up being used that don't obviously result in exploits but nevertheless result in exploits in practice. For the average developer, it's easiest just to avoid that quagmire altogether, rather than try to reason about the potential adversaries (and potentially get that wrong).
> OK, but a good sign that you're not using a cryptographic RNG is that you're somehow "seeding" it.
The change here is specific to the global RNG, which users often used without explicitly seeding - e.g. calling rand.Int() without first calling the now-deprecated rand.Seed(int64).
The distinction is obvious to people who have domain expertise here, but I've found many people make mistakes with it in practice, because it's easy to do.
That's not quite right. There is such a thing as a CSPRNG (https://en.wikipedia.org/wiki/Cryptographically_secure_pseud...). But you still have to seed it with another source of random (ideally non-computational) to actually get the "cryptographically secure" bit.
What? Cryptographic RNGs can be seeded, this is done all the time. Being able to seed a random number generator has no bearing on its cryptographic security.
As examples of secure cryptographic RNGs that can be seeded:
- Fortuna (has a seedable generator without the entropy pool parts)
The system feeds unpredictable bits into its kernel random number generator and then expands it, through /dev/urandom and getrandom. You, as a developer, shouldn't be messing with any of this. (Obviously: Keccak, AES, and ChaCha are not themselves CSPRNGs at all, but rather primitives cryptography engineers can use to create them).
If you're seeding an RNG, you're almost certainly working with a userland RNG, which is, almost always, a mistake.
I think this is more about interfaces than algorithms. A good cryptographic RNG interface will generally not expose the ability to explicitly set the seed for a given algorithm. Instead it would either abstract this entirely away, or provide some kind of interface for adding entropy to an entropy pool. The PRNGs themselves obviously do need to have seeds...
Sure, and that's even been in the documentation since before Go 1.0, yet people still make mistakes with it in practice. I've found it worth making the point explicit. Particularly in a case like this, where a casual reader might not notice the distinction between "non-reproducible" and "secure".