diff --git a/Makefile b/Makefile
index 06a6eb3..d3dbef1 100644
--- a/Makefile
+++ b/Makefile
@@ -17,7 +17,7 @@
P = otpcli
V = 0.0
-HDR = err.h strtonum.h otp.h
+HDR = base32.h err.h strtonum.h otp.h
OBJ = cli.o ${HDR:.h=.o}
SRC = ${OBJ:.o=.c}
diff --git a/base32.c b/base32.c
new file mode 100644
index 0000000..a823e4e
--- /dev/null
+++ b/base32.c
@@ -0,0 +1,101 @@
+/*
+ * otpcli - CLI utility for generating OTPs
+ *
+ * Written in 2021 by Lucas
+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to the
+ * public domain worldwide. This software is distributed without any
+ * warranty.
+ *
+ * You should have received a copy of the CC0 Public Domain Dedication
+ * along with this software. If not, see
+ * .
+ */
+
+#include
+#include
+#include
+
+#include "base32.h"
+
+#define B32_VALID(a) (('A' <= (a) && (a) <= 'Z') || ('2' <= (a) && (a) <= '7'))
+#define B32_DECODE_CHAR(a) ((a) <= '7' ? (a) - '2' + 26 : (a) - 'A')
+
+static int b32_last_chunk_length[] = { 0, -1, 1, -1, 2, 3, -1, 4 };
+
+static void
+b32_decode_chunk(unsigned char out[5], const char in[8])
+{
+ uint64_t x = 0;
+
+ x |= (uint64_t)B32_DECODE_CHAR(in[0]) << 35;
+ x |= (uint64_t)B32_DECODE_CHAR(in[1]) << 30;
+ x |= (uint64_t)B32_DECODE_CHAR(in[2]) << 25;
+ x |= (uint64_t)B32_DECODE_CHAR(in[3]) << 20;
+ x |= (uint64_t)B32_DECODE_CHAR(in[4]) << 15;
+ x |= (uint64_t)B32_DECODE_CHAR(in[5]) << 10;
+ x |= (uint64_t)B32_DECODE_CHAR(in[6]) << 5;
+ x |= (uint64_t)B32_DECODE_CHAR(in[7]);
+
+ out[0] = (x >> 32) & 0xff;
+ out[1] = (x >> 24) & 0xff;
+ out[2] = (x >> 16) & 0xff;
+ out[3] = (x >> 8) & 0xff;
+ out[4] = x & 0xff;
+}
+
+int
+b32_decoded_len(const char *s, size_t n)
+{
+ const char *it, *t;
+ size_t r, eqc;
+
+ for (it = s; *it != '\0' && B32_VALID(*it); it++)
+ continue;
+ if (*it != '\0' && *it != '=')
+ return -1;
+
+ for (t = it; *t == '=' && t - it < 8; t++)
+ continue;
+ eqc = t - it;
+ if (*t != '\0' || t - it == 8 || b32_last_chunk_length[eqc] == -1)
+ return -1;
+
+ r = n / 8 * 5 + b32_last_chunk_length[eqc == 0 ? n % 8 : eqc];
+ return r > INT_MAX ? -1 : r;
+}
+
+int
+b32_decode(unsigned char *out, size_t outlen, const char *in, size_t inlen)
+{
+ unsigned char tout[5];
+ char tin[8];
+ size_t i, t, eqc, last_chunk;
+
+ t = inlen;
+ while (in[inlen - 1] == '=')
+ inlen--;
+ eqc = t - inlen;
+ last_chunk = b32_last_chunk_length[eqc == 0 ? inlen % 8 : eqc];
+
+ while (inlen >= 8 && outlen >= 5) {
+ b32_decode_chunk(out, in);
+ in += 8;
+ inlen -= 8;
+ out += 5;
+ outlen -= 5;
+ }
+
+ if (inlen > 0) {
+ if (outlen == 0)
+ return 0;
+
+ memcpy(tin, in, inlen);
+ memset(tin + inlen, 'A', sizeof(tin) - inlen);
+ b32_decode_chunk(tout, tin);
+ memcpy(out, tout, outlen);
+ }
+
+ return outlen >= last_chunk;
+}
diff --git a/base32.h b/base32.h
new file mode 100644
index 0000000..965ee41
--- /dev/null
+++ b/base32.h
@@ -0,0 +1,2 @@
+int b32_decoded_len(const char *, size_t);
+int b32_decode(unsigned char *, size_t, const char *, size_t);