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 <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <time.h>
25 #include <unistd.h>
27 #include <openssl/evp.h>
28 #include <openssl/hmac.h>
30 #ifndef __OpenBSD__
31 # define pledge(a, b) (0)
32 #endif
34 #ifndef __dead
35 # define __dead __attribute__((noreturn))
36 #endif
38 #if defined(__FreeBSD__) || defined(__NetBSD__)
39 # include <sys/endian.h>
40 #elif defined(__APPLE__)
41 # include <machine/endian.h>
42 # include <libkern/OSByteOrder.h>
43 # define htobe64(x) OSSwapHostToBigInt64(x)
44 # define be32toh(x) OSSwapBigToHostInt32(x)
45 #else
46 # include <endian.h>
47 #endif
49 static __dead void
50 usage(void)
51 {
52 fprintf(stderr, "usage: %s\n", getprogname());
53 exit(1);
54 }
56 static int
57 b32c(unsigned char c)
58 {
59 if (c >= 'A' && c <= 'Z')
60 return (c - 'A');
61 if (c >= '2' && c <= '7')
62 return (c - '2' + 26);
63 errno = EINVAL;
64 return (-1);
65 }
67 static size_t
68 b32decode(const char *s, char *q, size_t qlen)
69 {
70 int i, val[8];
71 char *t = q;
73 while (*s != '\0') {
74 memset(val, 0, sizeof(val));
75 for (i = 0; i < 8; ++i) {
76 if (*s == '\0')
77 break;
78 if ((val[i] = b32c(*s)) == -1)
79 return (0);
80 s++;
81 }
83 if (qlen < 5) {
84 errno = ENOSPC;
85 return (0);
86 }
87 qlen -= 5;
89 *q++ = (val[0] << 3) | (val[1] >> 2);
90 *q++ = ((val[1] & 0x03) << 6) | (val[2] << 1) | (val[3] >> 4);
91 *q++ = ((val[3] & 0x0F) << 4) | (val[4] >> 1);
92 *q++ = ((val[4] & 0x01) << 7) | (val[5] << 2) | (val[6] >> 3);
93 *q++ = ((val[6] & 0x07) << 5) | val[7];
94 }
96 return (q - t);
97 }
99 /* adapted from httpd(8) */
100 static char *
101 url_decode(char *url, char *dst)
103 char *p, *q;
104 char hex[3];
105 unsigned long x;
107 hex[2] = '\0';
108 p = url;
109 q = dst;
111 while (*p != '\0') {
112 if (*p != '%') {
113 *q++ = *p++;
114 continue;
117 if (!isxdigit((unsigned char)p[1]) ||
118 !isxdigit((unsigned char)p[2]))
119 return (NULL);
121 hex[0] = p[1];
122 hex[1] = p[2];
124 /*
125 * We don't have to validate "hex" because it is
126 * guaranteed to include two hex chars followed by nul.
127 */
128 x = strtoul(hex, NULL, 16);
129 *q++ = (char)x;
130 p += 3;
132 *q = '\0';
133 return (url);
136 static int
137 uri2secret(char *s, int *digits, const EVP_MD **alg, int *period)
139 char *q, *t, *f, *secret = NULL;
140 const char *errstr;
142 if ((q = strchr(s, '?')) == NULL)
143 return (-1);
145 t = q + 1;
146 while ((f = strsep(&t, "&")) != NULL) {
147 if (!strncmp(f, "secret=", 7))
148 secret = f + 7;
149 else if (!strncmp(f, "digits=", 7)) {
150 f += 7;
151 if (!strcmp(f, "6"))
152 *digits = 6;
153 else if (!strcmp(f, "7"))
154 *digits = 7;
155 else if (!strcmp(f, "8"))
156 *digits = 8;
157 else
158 warnx("invalid number of digits; using 6");
159 } else if (!strncmp(f, "algorithm=", 10)) {
160 f += 10;
161 if (!strcmp(f, "SHA1"))
162 *alg = EVP_sha1();
163 else if (!strcmp(f, "SHA256"))
164 *alg = EVP_sha256();
165 else if (!strcmp(f, "SHA512"))
166 *alg = EVP_sha512();
167 else
168 warnx("unknown algorithm; using SHA1");
169 } else if (!strncmp(f, "period=", 7)) {
170 f += 7;
171 *period = strtonum(f, 1, 120, &errstr);
172 if (errstr) {
173 warnx("period is %s: %s; using 30", errstr, f);
174 *period = 30;
179 if (secret == NULL)
180 return (-1);
181 if (url_decode(secret, s) == NULL)
182 errx(1, "failed to percent-decode the secret");
183 return (0);
186 int
187 main(int argc, char **argv)
189 char buf[1024];
190 size_t buflen;
191 const EVP_MD *alg;
192 unsigned char md[EVP_MAX_MD_SIZE];
193 unsigned int mdlen;
194 char *s, *q, *line = NULL;
195 size_t linesize = 0;
196 ssize_t linelen;
197 uint64_t ct;
198 uint32_t hash;
199 uint8_t off;
200 int ch, digits = 6, period = 30;
202 if (pledge("stdio", NULL) == -1)
203 err(1, "pledge");
205 while ((ch = getopt(argc, argv, "")) != -1) {
206 switch (ch) {
207 default:
208 usage();
211 argc -= optind;
212 argv += optind;
214 if (argc != 0)
215 usage();
217 alg = EVP_sha1();
219 linelen = getline(&line, &linesize, stdin);
220 if (linelen == -1) {
221 if (ferror(stdin))
222 err(1, "getline");
223 errx(1, "no secret provided");
225 for (s = q = line; *s != '\0'; ++s) {
226 if (isspace((unsigned char)*s)) {
227 linelen--;
228 continue;
230 *q++ = *s;
232 *q = '\0';
233 if (linelen < 1)
234 errx(1, "no secret provided");
236 if (!strncmp(line, "otpauth://", 10) &&
237 uri2secret(line, &digits, &alg, &period) == -1)
238 errx(1, "failed to decode otpauth URI");
240 if ((buflen = b32decode(line, buf, sizeof(buf))) == 0)
241 err(1, "can't base32 decode the secret");
243 ct = htobe64(time(NULL) / period);
245 HMAC(alg, buf, buflen, (unsigned char *)&ct, sizeof(ct), md, &mdlen);
247 off = md[mdlen - 1] & 0x0F;
249 memcpy(&hash, md + off, sizeof(hash));
250 hash = be32toh(hash);
252 switch (digits) {
253 case 6:
254 printf("%06d\n", (hash & 0x7FFFFFFF) % 1000000);
255 break;
256 case 7:
257 printf("%07d\n", (hash & 0x7FFFFFFF) % 10000000);
258 break;
259 case 8:
260 printf("%08d\n", (hash & 0x7FFFFFFF) % 100000000);
261 break;
264 free(line);
265 return (0);