Blob


1 /*
2 * Copyright (c) 2022, 2023 Omar Polo <op@openbsd.org>
3 * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
18 #include <ctype.h>
19 #include <err.h>
20 #include <errno.h>
21 #include <limits.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <time.h>
26 #include <unistd.h>
28 #include <openssl/evp.h>
29 #include <openssl/hmac.h>
31 #ifndef __OpenBSD__
32 # define pledge(a, b) (0)
33 #endif
35 #ifndef __dead
36 # define __dead __attribute__((noreturn))
37 #endif
39 #if defined(__FreeBSD__) || defined(__NetBSD__)
40 # include <sys/endian.h>
41 #elif defined(__APPLE__)
42 # include <machine/endian.h>
43 # include <libkern/OSByteOrder.h>
44 # define htobe64(x) OSSwapHostToBigInt64(x)
45 # define be32toh(x) OSSwapBigToHostInt32(x)
46 #else
47 # include <endian.h>
48 #endif
50 static __dead void
51 usage(const char *argv0)
52 {
53 const char *me;
55 if ((me = strrchr(argv0, '/')) != NULL)
56 me++;
57 else
58 me = argv0;
60 fprintf(stderr, "usage: %s\n", me);
61 exit(1);
62 }
64 static int
65 b32c(unsigned char c)
66 {
67 if (c >= 'A' && c <= 'Z')
68 return (c - 'A');
69 if (c >= '2' && c <= '7')
70 return (c - '2' + 26);
71 errno = EINVAL;
72 return (-1);
73 }
75 static size_t
76 b32decode(const char *s, char *q, size_t qlen)
77 {
78 int i, val[8];
79 char *t = q;
81 while (*s != '\0') {
82 memset(val, 0, sizeof(val));
83 for (i = 0; i < 8; ++i) {
84 if (*s == '\0')
85 break;
86 if ((val[i] = b32c(*s)) == -1)
87 return (0);
88 s++;
89 }
91 if (qlen < 5) {
92 errno = ENOSPC;
93 return (0);
94 }
95 qlen -= 5;
97 *q++ = (val[0] << 3) | (val[1] >> 2);
98 *q++ = ((val[1] & 0x03) << 6) | (val[2] << 1) | (val[3] >> 4);
99 *q++ = ((val[3] & 0x0F) << 4) | (val[4] >> 1);
100 *q++ = ((val[4] & 0x01) << 7) | (val[5] << 2) | (val[6] >> 3);
101 *q++ = ((val[6] & 0x07) << 5) | val[7];
104 return (q - t);
107 /* adapted from httpd(8) */
108 static char *
109 url_decode(char *url, char *dst)
111 char *p, *q;
112 char hex[3];
113 unsigned long x;
115 hex[2] = '\0';
116 p = url;
117 q = dst;
119 while (*p != '\0') {
120 if (*p != '%') {
121 *q++ = *p++;
122 continue;
125 if (!isxdigit((unsigned char)p[1]) ||
126 !isxdigit((unsigned char)p[2]))
127 return (NULL);
129 hex[0] = p[1];
130 hex[1] = p[2];
132 /*
133 * We don't have to validate "hex" because it is
134 * guaranteed to include two hex chars followed by nul.
135 */
136 x = strtoul(hex, NULL, 16);
137 *q++ = (char)x;
138 p += 3;
140 *q = '\0';
141 return (url);
144 static int
145 uri2secret(char *s, int *digits, const EVP_MD **alg, int *period)
147 char *q, *t, *f, *ep, *secret = NULL;
148 long l;
150 if ((q = strchr(s, '?')) == NULL)
151 return (-1);
153 t = q + 1;
154 while ((f = strsep(&t, "&")) != NULL) {
155 if (!strncmp(f, "secret=", 7))
156 secret = f + 7;
157 else if (!strncmp(f, "digits=", 7)) {
158 f += 7;
159 if (!strcmp(f, "6"))
160 *digits = 6;
161 else if (!strcmp(f, "7"))
162 *digits = 7;
163 else if (!strcmp(f, "8"))
164 *digits = 8;
165 else
166 warnx("invalid number of digits; using 6");
167 } else if (!strncmp(f, "algorithm=", 10)) {
168 f += 10;
169 if (!strcmp(f, "SHA1"))
170 *alg = EVP_sha1();
171 else if (!strcmp(f, "SHA256"))
172 *alg = EVP_sha256();
173 else if (!strcmp(f, "SHA512"))
174 *alg = EVP_sha512();
175 else
176 warnx("unknown algorithm; using SHA1");
177 } else if (!strncmp(f, "period=", 7)) {
178 f += 7;
179 errno = 0;
180 l = strtol(f, &ep, 10);
181 if (f[0] == '\0' || *ep != '\0')
182 err(1, "period is not a number: %s", f);
183 if (errno == ERANGE && (l == LONG_MAX || l == LONG_MIN))
184 err(1, "period is way out of range: %s", f);
185 if (l < 1 || l > 120)
186 err(1, "period is out of range: %s", f);
187 *period = l;
191 if (secret == NULL)
192 return (-1);
193 if (url_decode(secret, s) == NULL)
194 errx(1, "failed to percent-decode the secret");
195 return (0);
198 int
199 main(int argc, char **argv)
201 char buf[1024];
202 size_t buflen;
203 const EVP_MD *alg;
204 unsigned char md[EVP_MAX_MD_SIZE];
205 unsigned int mdlen;
206 const char *argv0;
207 char *s, *q, *line = NULL;
208 size_t linesize = 0;
209 ssize_t linelen;
210 uint64_t ct;
211 uint32_t hash;
212 uint8_t off;
213 int ch, digits = 6, period = 30;
215 if (pledge("stdio", NULL) == -1)
216 err(1, "pledge");
218 if ((argv0 = argv[0]) == NULL)
219 argv0 = "totp";
221 while ((ch = getopt(argc, argv, "")) != -1) {
222 switch (ch) {
223 default:
224 usage(argv0);
227 argc -= optind;
228 argv += optind;
230 if (argc != 0)
231 usage(argv0);
233 alg = EVP_sha1();
235 linelen = getline(&line, &linesize, stdin);
236 if (linelen == -1) {
237 if (ferror(stdin))
238 err(1, "getline");
239 errx(1, "no secret provided");
241 for (s = q = line; *s != '\0'; ++s) {
242 if (isspace((unsigned char)*s)) {
243 linelen--;
244 continue;
246 *q++ = *s;
248 *q = '\0';
249 if (linelen < 1)
250 errx(1, "no secret provided");
252 if (!strncmp(line, "otpauth://", 10) &&
253 uri2secret(line, &digits, &alg, &period) == -1)
254 errx(1, "failed to decode otpauth URI");
256 if ((buflen = b32decode(line, buf, sizeof(buf))) == 0)
257 err(1, "can't base32 decode the secret");
259 ct = htobe64(time(NULL) / period);
261 HMAC(alg, buf, buflen, (unsigned char *)&ct, sizeof(ct), md, &mdlen);
263 off = md[mdlen - 1] & 0x0F;
265 memcpy(&hash, md + off, sizeof(hash));
266 hash = be32toh(hash);
268 switch (digits) {
269 case 6:
270 printf("%06d\n", (hash & 0x7FFFFFFF) % 1000000);
271 break;
272 case 7:
273 printf("%07d\n", (hash & 0x7FFFFFFF) % 10000000);
274 break;
275 case 8:
276 printf("%08d\n", (hash & 0x7FFFFFFF) % 100000000);
277 break;
280 free(line);
281 return (0);