Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Author here: I was curious what HN would think of this token style, we're currently using it for getseam.com . I think it has a lot of advantages over other token styles (e.g. double-click to select, standard alphabet, compatible with secret scanning). Although this is a typescript library, we originally designed this API key for use in Ruby on Rails and the pattern should be fairly portable.


Thanks for sharing! As somebody who has been bitten before by copy-pasting a key I appreciate that as the headlining benefit :)

Thoughts from an ergonomic perspective:

- Base58 is nice and makes easy-to-handle values which is nice

- Naming of shortToken, longToken, and token are confusing to me, because it makes it sound like all of these values are tokens/secrets of some sort, rather than components of the token. Not to bikeshed, but what jumps to my mind is more like "prefix_id_secret", which helps make it clear what role each one plays.

- I don't think keyPrefix should have a default, I think your function should raise an error if it's not provided. Otherwise more than one user of your library is going to end up with "mycompany" keys in the wild.

- You probably ought to validate that keyPrefix does not contain an underscore and raise an exception if it does, otherwise that way lies pain for people trying to handle these keys.

Thoughts from a security perspective:

- You should probably use crypto.timingSafeEquals(buffer, buffer) [1] instead of comparing strings

- If you're trying to store the secret's hash, isn't something like bcrypt/scrypt preferred over raw sha256 these days?

[1] https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b


> If you're trying to store the secret's hash, isn't something like bcrypt/scrypt preferred over raw sha256 these days?

Not for this type of use case. If a secret is long and generated from a cryptographically secure source (eg /dev/urandom), then any cryptographic hash function is fine as brute forcing the secret itself is not feasible. A single pass of sha256 is fine and presumably you’d be doing many of these operations each second in a high throughput application.

“Slow” hash functions like bcrypt or scrypt are for user provided secrets that might not have a large amount of entropy. It’s fine for something like user authentication but would be way too slow and pointless for API keys.


> If a secret is long

It seems this one, at 24 base-58 characters, is only roughly twice as long as it needs to be, right? How short is too short?


Appreciate the thoughts on the naming/exceptions and thanks for taking a look at the implementation! Definitely making some adjustments tonight.

I think your thought on bcrypt/scrypt vs SHA256 is super interesting here. The long token is treated a lot like a password, so we should treat it similarly and use slow hashing. However, unlike a password an API key is repeatedly used for authentication instead of being exchanged for a session token. I don't think this meaningfully changes anything- so I think you're right that bcrypt/scrypt would be a better choice!

Edit: Also see koomla's answer!


What meaningfully changes the requirements is that takes many more attempts to brute force an API key because they're longer.

I'd consider using a weaker bcrypt/scrypt/argon2 for API keys than would be used for login. Perhaps one that takes a hundredth or a thousandth of the time.

It could be unnecessary though.

Here's the main scenario: someone snagged a hashed long token from a database backup and wants to get the unhashed long token so they can use it to access something behind the API. They can do all the brute forcing they want and the server owner will never know about it. There are 1 with 42 zeroes worth of potential long tokens to try (58 * '51FwqftsmMDHHbJAMEXXHCgG'.length). Seems unlikely even though it's very very cheap to hash a potential long token. The tokens this is using are pretty short, but still not short enough to make cracking it feasible.

If you used argon2 maybe you could cut mycompany_BRTRKFsL_51FwqftsmMDHHbJAMEXXHCgG down to mycompany_BRTRKFsL_51FwqftsmMDH. Shorter token!

I would perhaps make the long token 58 digits long just because it would have more than a googol (10^100) possible values but still be shorter than 80 characters with the prefix and short token, and maybe swap the SHA hashing for a low cost (memory and CPU) argon2.

If you wanted to have really short API keys you could get creative with argon2. This is relevant for magic links.

https://startdebugging.net/2013/10/counting-up-to-one-trilli...


Couple comments upon looking at the actual code:

- You can promisify randomBytes once and reuse it rather than twice for every invocation

- There shouldn’t be a default value for the company name or people will end up using it.

- The company name isn’t validated so it could contain underscores which would cause issues with the short token parsing as it assumes it’s the second “chunk”

- The equals comparison of the hashes for the secrets is not timing safe. It’s not as bad as if they were plain text but it does short circuit due to how string equals works. Use the actual built in timing safe equals on the Buffer hash (not the stringified hex).


thanks for taking a look! I've created an issue on the repo and should be able to address these tonight :)


On iPhone the long-press-to-select doesn’t cross the underscore boundary, so you end up just selecting part of the token. Maybe you can do without those underscores?


**shakes fist at sky/apple**


Can you tell more about the prefix? Not very clear to me. Is it issuer id or user id? And if it's later and is user provided, how it helps with scanning


It's the issuers company or product name. GitHub for example prefix their tokens with "gh_" which makes it easy to scan for tokens uploaded to repos or even across the web if they wanted.

GitHub has secret scanning features built in now, with a prefix of your product name or company name you could easily create a regex of sorts to find when someone has uploaded an API key for your application to their GitHub repo and revoke the token or email them.


Don’t use base58. Even bitcoin, which originated or at least popularized base58 has moved away from it. They are a pain to encode and decode, with a naive implementation requiring a bignum library, and they don’t save you much in the end over a base32.


But you don't need to encode or decode anything here? Tokens are just random strings?


How do you think the token is generated if not through an encoding process?

Other disadvantage I forgot to mention earlier: base58 is variable length, which is a foot gun that will bite you eventually.


Using base58 here strikes me as simply unnecessary. You could just use the base58 alphabet and directly generate random strings in that alphabet, easily and cheaply. The only thing you loose is the ability to decode into a bit shorter binary string for storage - almost certainly not worth it.


If you are prefixing it, why not prefix it with a full domain name? mycompany_com_BRTRKFsL_51FwqftsmMDHHbJAMEXXHCgG is not much longer than mycompany_BRTRKFsL_51FwqftsmMDHHbJAMEXXHCgG and allows reporting leaked secrets (for example using the GitHub scheme [1]) without having to look up the URL in some sort of registry, e.g. POST https://mycompany.com/.well-known/report-leaked-secrets

[1]: https://docs.github.com/en/developers/overview/secret-scanni...

edit: See also the discussion from last time: https://news.ycombinator.com/item?id=28296864


I think this is an excellent idea! However, we didn't like the aesthetic of the version that included the domain- I'll admit it's not great reasoning.


I don't follow what the security property is of involving sha256 against the b58 encoded value? If it serves as a checksum against tpyoss, wouldn't one wish to include the shortToken also? Otherwise, it's just as much attacker controlled content as the b58 version is, as best I can tell

As an implementation observation, I am also on a life-long campaign to rid the world of `split(...)[-1]` type manipulations, because they lack the context of a more rigorous "parsing" style. In this specific case, it allows attackers to smuggle almost arbitrary characters between the shortToken and longToken:

    checkAPIKey("alpha_BRTRKFsL_and this one time at band camp\r\nContent-Length: 0\r\n_51FwqftsmMDHHbJAMEXXHCgG",
    "d70d981d87b449c107327c2a2afbf00d4b58070d6ba571aac35d7ea3e7c79f37")


Also for your consideration, the code currently has duplication:

    export const extractLongTokenHash = (token: string) =>
      hashLongToken(extractLongToken(token))
    // ...snip...
      longTokenHash: hashLongToken(extractLongToken(token)),
    // ...snip...
    ) => hashLongToken(extractLongToken(token)) === expectedLongTokenHash
and my experience is that it's so easy to remember to update one and forget to update the others


Thanks, yes I agree the ".split(_)[-1]" is ugly and there should be validation on the key prior to operations on it!

Making an issue :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: