Concepts
The handful of ideas every page here leans on: what the four kinds of hashing are for, what "untrusted" means in this library, why timing matters, where random numbers come from, and what a "bit" of a secret measures.
You don't need a cryptography background to use unsecure. You do need to know which tool fits which job, because from a distance they all look the same: they turn unreadable stuff into more unreadable stuff. This page is the map. Every other page links back here instead of explaining things again, so if a term on some page feels unfamiliar, it's probably explained here.
#Four things that look like hashing
| You want to | Use | Secret involved | Reversible |
|---|---|---|---|
| Fingerprint data so you can tell later whether it changed | hash (SHA-256) | No | No |
| Prove a message came from someone holding a shared secret | hmac | Yes, a key | No |
| Turn one secret into several keys for different purposes | hkdf | Yes, the root secret | No |
| Store a password so a guess can be checked but the password can't be read back | argon2Hash | Optional pepper (the secret option) | No, and slow on purpose |
The last row is the one people get wrong, and it's worth a minute. A plain hash is fast, and fast is what you don't want for passwords. People pick from a small pile of likely passwords, so whoever steals your table tries the pile against every row, as fast as their hardware goes. Argon2 is deliberately slow and hungry for memory, so every single guess costs real time and real memory. That's the whole trick.
#Bits and what they measure
The word "bits" shows up on these pages meaning two different things, and mixing them up leads to bad decisions:
- How strong a generated secret is: how many equally likely values the generator could have handed you, written as a power of two. A 32-character token drawn from 62 characters has 62^32 possible values, about 190 bits. Nobody is guessing that, so you can store it as a fast digest with
hashand skip Argon2: there's no small pile of likely values to try first. - A guess at how good a value a person chose is:
entropy()looks at how varied the characters in a string are. It can tell you a password is weak; it can't tell you one is strong, because a line from a famous book scores great and is still easy to guess. Use it to say no, never to say yes.
Passwords are the second kind, so they get Argon2. Random tokens and recovery codes are the first kind, so a digest does the job.
#Trusted and untrusted input
Every function here deals with two kinds of values, and it treats them very differently:
- Your configuration: the secret, the algorithm name, a window size, a cost parameter. Get one of those wrong and that's your bug, so the function throws an
UnsecureErrorwith acodethat says what's wrong. - Something a client sent: a password guess, a six-digit code, a signature header, a JSON body. A bad value there isn't a bug, it's someone trying their luck. The verifying functions answer
falseand never throw.
So you can point hmacVerify, secureCompare, hotpVerify and totpVerify straight at request data with no try around them. argon2Verify is the one exception, because it also reads a stored hash that you own: a stored value it can't read throws MALFORMED, UNSUPPORTED or OUT_OF_RANGE, and a password that isn't text throws INVALID_TYPE.
#Constant time
Comparing two secrets with === stops at the first byte that differs. That sounds harmless until someone times it: a slightly slower answer means more leading bytes were right, so a guess can grow one byte at a time. secureCompare does the same amount of work no matter what you hand it, and every verify function in this library compares through it. The same idea is why totpVerify keeps checking every step in its window even after it has found a match.
#Where randomness comes from
Math.random() isn't random in the way you need: see a handful of its outputs and you can work out the next ones. It must never produce a secret. Everything random in unsecure comes from the platform's cryptographic generator, crypto.getRandomValues, which is built for exactly that.
Turning random bytes into a number in a range has a trap of its own. byte % 6 looks fine and isn't: 256 doesn't divide by 6, so 0 to 3 come up 43 times each and 4 and 5 only 42. secureRandomNumber(6) draws again whenever a byte would land in that unfair tail, so every value is equally likely.
#Encodings are not security
Hex, base64 and base32 change how bytes are written down, not what they are. They matter here for one reason: a signature or a secret arrives as text, and what you compare are the bytes behind it. The codecs decode strictly by default and reject text that no encoder could have written, such as base64 with bits set past the last byte. If you need two texts to be equal, compare the bytes, or store the spelling you produced yourself.
#What this library is not
- It doesn't encrypt. For encryption, use the Web Crypto API directly, or a library built for that.
- It has no JWT, JWS, JWE or JWK. unjwt covers those with the same approach and is meant to be used alongside this library.
- It holds no state. Replay protection for one-time passwords, rate limits and session storage are yours to build; the pages that need state tell you what to persist.