{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/totp-client/",
"description": "Build a TOTP-based 2FA client in Go using the standard library. Generate time-based one-time passwords like Google Authenticator.",
"path": "/go/totp-client/",
"publishedAt": "2023-08-20T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"TIL",
"Security"
],
"textContent": "A [TOTP] based 2FA system has two parts. One is a client that generates the TOTP code. The\nother part is a server. The server verifies the code. If the client and the server-generated\ncodes match, the server allows the inbound user to access the target system. The code\nusually expires after 30 seconds and then, you'll have to regenerate it to be able to\nauthenticate.\n\nAs per [RFC-6238], the server shares a base-32 encoded secret key with the client. A valid\nkey only contains characters from the Base32 alphabet, which is A-Z and 2-7. Using this\nshared secret and the current UNIX timestamp, the client generates a 6-digit code.\nIndependently, the server also generates a 6-digit code using the same secret string and its\nown current timestamp. If the user-entered client code matches the server-generated code,\nthe auth succeeds. Otherwise, it fails. The client's and the server's current timestamp\nwouldn't be an exact match. So the algorithm usually adjusts it for ~30 seconds duration.\n\nI wanted to see if I could write a TOTP client and use it like [Google Authenticator] to log\ninto my [2FA-enabled GitHub account]. Turns out Go's standard library lets you do that with\nonly a couple of lines of code. Here's the fully annotated implementation:\n\nUse it as such:\n\nThis prints the following code and will keep printing the same one for the next 30 seconds\nif you rerun the script multiple times:\n\nHere are the detailed implementation steps:\n\n- Trim whitespace and convert the base32 encoded secret key string to uppercase\n- Decode the preprocessed secret key from base32 to a byte slice\n- Get the current timestamp, divide by 30, and convert it to an 8-byte big-endian unsigned\n integer\n- Concatenate the timestamp integer bytes with the decoded secret key bytes\n- Hash the concatenated bytes to get a 20-byte [SHA-1] digest\n- Get the last byte of the SHA-1 digest and AND it with 0x0F (15) to mask off all but the\n last 4 bits to get an offset index from 0-15\n- Use the offset index to truncate the SHA-1 digest to get a 32-bit unsigned integer\n- AND the 32-bit integer with 0x7FFFFFFF (2147483647) to mask off the most significant bit\n and convert to an unsigned 31-bit integer\n- Take modulo 1_000_000 of the 31-bit integer to get a 6-digit TOTP code\n- Return the 6-digit TOTP code\n\nTo test the implementation, I collected a secret key from [GitHub's 2FA panel]. Then I\nlogged into my account by inputting a TOTP code generated by this script. Worked flawlessly!\n\n\n\n\n\n[totp]:\n https://www.twilio.com/docs/glossary/totp\n\n\n[rfc-6238]:\n https://datatracker.ietf.org/doc/html/rfc6238\n\n[google authenticator]:\n https://apps.apple.com/us/app/google-authenticator/id388497605\n\n\n[2fa-enabled github account]:\n https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa\n\n[sha-1]:\n https://www.rfc-editor.org/rfc/rfc3174.html\n\n[github's 2fa panel]:\n https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/configuring-two-factor-authentication",
"title": "Writing a TOTP client in Go"
}