├── COPYING ├── README.md └── ga /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2014-2015 David Mazières. You may use and distribute 3 | this code under the terms of the GPLv3 or later (available from 4 | ), and additionally you may 5 | link against openssl libcrypto. 6 | 7 | This program comes with ABSOLUTELY NO WARRANTY. This program REDUCES 8 | YOUR SECURITY by making two-factor authentication not really two 9 | factor. You may decide using this program is useful where the 10 | alternative is only one factor authentication, but that is your 11 | decision alone. The author DOES NOT REPRESENT THIS SOFTWARE AS MORE 12 | SECURE THAN ANY ALTERNATIVE; it most likely is less secure. 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ga: Google Authenticator in the Shell 2 | 3 | Generate two factor authentication codes that are compatible with 4 | Google Authenticator (a.k.a. RFC6238 TOTP) from the shell. 5 | 6 | ## References 7 | 8 | [RFC6238](https://tools.ietf.org/html/rfc6238) 9 | 10 | ## Installing 11 | 12 | Just download and install 13 | [`ga`](https://raw.githubusercontent.com/scslab/ga/master/ga) 14 | somewhere on your path (e.g., `$HOME/bin/ga`). 15 | 16 | Requirements: 17 | 18 | * gnupg 19 | 20 | Optional dependencies: 21 | 22 | * xclip - to send login codes to the X clipboard instead of stdout 23 | * zbar - to parse QR codes from screen with -c option 24 | * qrencode, imagemagick - to display QR codes with -v option 25 | 26 | ## Using 27 | 28 | * `ga [-x]` _service_ - get token for _service_ 29 | * `ga -[x]c` _service_ - record seed for new _service_ 30 | * `ga -[x]v` _service_ - view qrcode for _service_ 31 | * `ga -l` - list services setup 32 | 33 | The `-x`, `-xc`, and `-xv` flags disable use of X11 even when 34 | `$DISPLAY` is set, compared to no option, `-c`, and `-v`. 35 | 36 | ## OS X and GPG Tools 37 | 38 | Mac OS X users may be using the [GPG Tools](https://gpgtools.org/) 39 | distribution. For some strange reason, this package doesn't detect 40 | when you are running in a terminal session without a display (e.g., 41 | SSH), so by default, it always launches a GUI for key entry. Which 42 | makes `ga` impossible to use over SSH on OS X. 43 | 44 | To fix this, you'll want to tell it to uses a curses display for 45 | passpharse entry: 46 | 47 | export PINENTRY_USER_DATA="USE_CURSES=1" 48 | 49 | A good way to decide when to export this variable is by detecting it 50 | `SSH_CONNECTION` is a non-empty string. 51 | 52 | ## Get involved! 53 | 54 | We are happy to receive bug reports, fixes, documentation enhancements, 55 | and other improvements. 56 | 57 | Please report bugs via the 58 | [github issue tracker](http://github.com/scslab/ga/issues). 59 | 60 | Master [git repository](http://github.com/scslab/ga): 61 | 62 | * `git clone git://github.com/scslab/ga.git` 63 | 64 | ## Authors 65 | 66 | David Mazières. 67 | -------------------------------------------------------------------------------- /ga: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # ga - manage Google authenticator (RFC6238) seeds 4 | # 5 | # Copyright 2014 David Mazieres. You may use and distribute this code 6 | # under the terms of the GPLv3 or later, and additionally you may link 7 | # against openssl libcrypto. 8 | # 9 | # This program comes with ABSOLUTELY NO WARRANTY. This program 10 | # REDUCES YOUR SECURITY by making two-factor authentication not really 11 | # two factor. You may decide using this program is useful where the 12 | # alternative is only one factor authentication, but that is your 13 | # decision alone. The author DOES NOT REPRESENT THIS SOFTWARE AS MORE 14 | # SECURE THAN ANY ALTERNATIVE; it most likely is less secure. 15 | 16 | # Installation: 17 | # Just put it somewhere on your path (e.g., $HOME/bin/ga). 18 | # 19 | # Requirements: 20 | # gnupg 21 | # 22 | # Optional dependencies: 23 | # xclip, xsel - to send login codes to the X clipboard instead of stdout 24 | # pbcopy - to send login codes to the OSX clipboard instaed of stdout 25 | # zbar - to parse QR codes from screen with -c option 26 | # qrencode, imagemagick - to display QR codes with -v option 27 | 28 | # Tip: If you need to retrieve seeds from an android phone, try 29 | # running the following command as root on your phone: 30 | # 31 | # sqlite3 -header \ 32 | # /data/data/com.google.android.apps.authenticator2/databases/databases \ 33 | # 'SELECT email, secret FROM accounts;' 34 | 35 | 36 | exe=google-auth-code 37 | suffix=$(uname -sm | sed -e 's/ /-/g') 38 | prog=$(basename $0) 39 | keydir="${XDG_CONFIG_HOME-${HOME}/.config}/ga" 40 | bindir="$keydir/bin" 41 | gpgcmd="gpg" 42 | gpgoptions="--pinentry-mode loopback -q --no-mdc-warning" 43 | 44 | usage() { 45 | echo "usage: $prog [-x] service # get token for service" >&2 46 | echo " $prog -[x]c service # record seed for new service" >&2 47 | echo " $prog -[x]v service # view qrcode for service" >&2 48 | echo " $prog -l # list services setup" >&2 49 | echo "(The -x* flags disable use of X11 even when \$DISPLAY is set.)" >&2 50 | exit 1 51 | } 52 | 53 | if [[ "$OSTYPE" == "darwin"* ]]; then 54 | export DISPLAY="" 55 | fi 56 | 57 | case "$1" in 58 | -x) 59 | shift 60 | unset DISPLAY 61 | ;; 62 | -x*) 63 | unset DISPLAY 64 | ;; 65 | esac 66 | test "$#" -ge 1 -a "$#" -le 2 || usage 67 | 68 | case $(gsettings get org.gnome.crypto.cache gpg-cache-method 2>/dev/null) in 69 | "'idle'"|"'timeout'"|"") 70 | ;; 71 | *) 72 | cat >&2 <&2 82 | exit 1; 83 | fi 84 | 85 | if ! test "$bindir/$exe.$suffix" -nt "$self"; then 86 | mkdir -p "$bindir" 87 | sed -e '1,/^BEGIN CODE/d' "$self" > "$bindir/$exe.c" 88 | cc_cmd=cc 89 | if test -n "$CC"; then 90 | cc_cmd=$CC 91 | fi 92 | $cc_cmd $(pkg-config --cflags libcrypto) -Wall -g \ 93 | -o "$bindir/$exe.$suffix" "$bindir/$exe.c" \ 94 | $(pkg-config --libs libcrypto) \ 95 | || exit 1 96 | fi 97 | 98 | if test "$#" -eq 1 -a x"$1" != x"-l"; then 99 | mode="" 100 | target="$1" 101 | else 102 | mode="$1" 103 | target="$2" 104 | fi 105 | 106 | gk() { 107 | while read line; do 108 | nosecret="${line%%\?*}" 109 | account="${nosecret#QR-Code:otpauth://totp/}" 110 | if test x"$nosecret" != x"$account"; then 111 | echo -n "Use $account? (y) " > /dev/tty 112 | read ans < /dev/tty 113 | if test x"$ans" = x -o y = "$ans" -o yes = "$ans"; then 114 | echo "${line#QR-Code:}" 115 | return 116 | fi 117 | fi 118 | done 119 | } 120 | 121 | case "$mode" in 122 | "") 123 | if code=$($gpgcmd $gpgoptions -d "$keydir/$target.gpg" \ 124 | | "$bindir/$exe.$suffix" -); then 125 | if test -n "$DISPLAY" && command -v xclip > /dev/null; then 126 | echo "Sending code to X clipboard" 127 | echo -n "$code" | xclip -i 128 | elif test -n "$DISPLAY" && command -v xsel > /dev/null; then 129 | echo "Sending code to X clipboard" 130 | echo -n "$code" | xsel -i 131 | elif test -n "$DISPLAY" && command -v pbcopy > /dev/null; then 132 | echo "Sending code to OSX clipboard" 133 | printf "$code" | pbcopy 134 | else 135 | echo "$code" 136 | fi 137 | else 138 | exit 1 139 | fi 140 | ;; 141 | -c|-xc) 142 | umask 077 143 | if test -f "$keydir/$target.gpg"; then 144 | echo "$keydir/$target.gpg exists already; please delete it." 145 | exit 1 146 | fi 147 | mkdir -p "$keydir" 148 | unset key 149 | if test -n "$DISPLAY"; then 150 | key=$(zbarimg x:root 2> /dev/null | gk) 151 | fi 152 | if test -z "$key"; then 153 | echo -n "Enter Base32-encoded authenticator key: " 154 | read key 155 | if test -z "$key"; then 156 | echo "Aborted." 157 | exit 0 158 | fi 159 | fi 160 | echo "key is $key." 161 | 162 | echo "$key" | "$bindir/$exe.$suffix" - 0 || exit 1 163 | echo "$key" | $gpgcmd $gpgoptions -c -o "$keydir/$2.gpg" 164 | ;; 165 | -v|-xv) 166 | secret=$($gpgcmd $gpgoptions -d "$keydir/$target.gpg") || exit 1 167 | case "$secret" in 168 | otpauth://totp/*) 169 | ;; 170 | *) 171 | secret="otpauth://totp/$target?secret=$secret&issuer=$target" 172 | ;; 173 | esac 174 | if test -z "$DISPLAY"; then 175 | printf "$secret" | tr -d ' ' | qrencode -t ansi 176 | elif command -v display > /dev/null; then 177 | printf "$secret" | tr -d ' ' | qrencode -o- | display - 178 | elif command -v xv > /dev/null; then 179 | printf "$secret" | tr -d ' ' | qrencode -o- | xv - 180 | elif command -v xloadimage > /dev/null; then 181 | printf "$secret" | tr -d ' ' | qrencode -o- | xloadimage stdin 182 | else 183 | printf "$secret" | tr -d ' ' | qrencode -t ansi 184 | fi 185 | ;; 186 | -l) 187 | ls "$keydir/" | sed -ne "s/.gpg$//p" 188 | ;; 189 | *) 190 | usage 191 | ;; 192 | esac 193 | 194 | exit 0 195 | BEGIN CODE 196 | /* Google authenticator code calculation, link with -lcrypto */ 197 | 198 | #include 199 | #include 200 | #include 201 | #include 202 | #include 203 | #include 204 | #include 205 | 206 | #ifdef __APPLE__ 207 | #include 208 | #include 209 | #else 210 | #include 211 | #include 212 | #endif 213 | 214 | char *progname; 215 | 216 | static const signed char a2b32[256] = { 217 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 218 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 219 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 220 | -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, 221 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 222 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 223 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 224 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, 225 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 226 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 227 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 228 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 229 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 230 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 231 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 232 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 233 | }; 234 | 235 | static const int a2b32rem[8] = {0, -1, 1, -1, 2, 3, -1, 4}; 236 | 237 | ssize_t 238 | armor32len (const char *_s) 239 | { 240 | const u_char *s = (const u_char *) _s; 241 | const u_char *p = s; 242 | ssize_t len; 243 | while (a2b32[*p] >= 0) 244 | p++; 245 | len = p - s; 246 | return a2b32rem[len & 7] < 0 ? -1 : len; 247 | } 248 | 249 | ssize_t 250 | dearmor32len (const char *s) 251 | { 252 | ssize_t len = armor32len (s); 253 | return len < 0 ? -1 : (len >> 3) * 5 + a2b32rem[len & 7]; 254 | } 255 | 256 | ssize_t 257 | dearmor32 (void *out, const char *_s) 258 | { 259 | const u_char *s = (const u_char *) _s; 260 | ssize_t len = armor32len (_s); 261 | int rem = a2b32rem [len & 7]; 262 | size_t outlen = (len >> 3) * 5 + rem; 263 | char *d = out; 264 | int c0, c1, c2, c3, c4, c5, c6, c7; 265 | const u_char *e; 266 | 267 | if (rem < 0) 268 | return -1; 269 | if (len <= 0) 270 | return 0; 271 | 272 | for (e = s + (len & ~7); s < e; s += 8, d += 5) { 273 | c0 = a2b32[s[0]]; 274 | c1 = a2b32[s[1]]; 275 | d[0] = c0 << 3 | c1 >> 2; 276 | c2 = a2b32[s[2]]; 277 | c3 = a2b32[s[3]]; 278 | d[1] = c1 << 6 | c2 << 1 | c3 >> 4; 279 | c4 = a2b32[s[4]]; 280 | d[2] = c3 << 4 | c4 >> 1; 281 | c5 = a2b32[s[5]]; 282 | c6 = a2b32[s[6]]; 283 | d[3] = c4 << 7 | c5 << 2 | c6 >> 3; 284 | c7 = a2b32[s[7]]; 285 | d[4] = c6 << 5 | c7; 286 | } 287 | 288 | if (rem >= 1) { 289 | c0 = a2b32[s[0]]; 290 | c1 = a2b32[s[1]]; 291 | *d++ = c0 << 3 | c1 >> 2; 292 | if (rem >= 2) { 293 | c2 = a2b32[s[2]]; 294 | c3 = a2b32[s[3]]; 295 | *d++ = c1 << 6 | c2 << 1 | c3 >> 4; 296 | if (rem >= 3) { 297 | c4 = a2b32[s[4]]; 298 | *d++ = c3 << 4 | c4 >> 1; 299 | if (rem >= 4) { 300 | c5 = a2b32[s[5]]; 301 | c6 = a2b32[s[6]]; 302 | *d++ = c4 << 7 | c5 << 2 | c6 >> 3; 303 | } 304 | } 305 | } 306 | } 307 | 308 | assert (d == (char *) out + outlen); 309 | return len; 310 | } 311 | 312 | int 313 | dearmor32a (void **outp, size_t *outlen, char *in) 314 | { 315 | if (armor32len (in) != strlen (in)) 316 | return 0; 317 | dearmor32 ((*outp = malloc ((*outlen = dearmor32len (in)))), in); 318 | return 1; 319 | } 320 | 321 | uint32_t 322 | hotp (const void *key, size_t keylen, unsigned long val) 323 | { 324 | int i; 325 | unsigned char valbytes[8]; 326 | unsigned char hash[20]; 327 | unsigned char *p; 328 | for (i = 8; i--; val >>= 8) 329 | valbytes[i] = val; 330 | #ifdef __APPLE__ 331 | CCHmac (kCCHmacAlgSHA1, key, keylen, valbytes, sizeof (valbytes), hash); 332 | #else 333 | HMAC (EVP_sha1 (), key, keylen, valbytes, sizeof (valbytes), hash, NULL); 334 | #endif 335 | p = hash + (hash[sizeof (hash) - 1] & 0xf); 336 | return ((p[0] & 0x7f) << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; 337 | } 338 | 339 | static const char * 340 | findsecret(const char *in) 341 | { 342 | static const char totp[] = "otpauth://totp/"; 343 | static const char qsecret[] = "?secret="; 344 | static const char asecret[] = "&secret="; 345 | const char *start; 346 | if (strncmp (in, totp, sizeof(totp) - 1)) 347 | return in; 348 | if ((start = strstr(in, qsecret))) 349 | return start + sizeof(qsecret) - 1; 350 | if ((start = strstr(in, asecret))) 351 | return start + sizeof(asecret) - 1; 352 | return start; 353 | } 354 | 355 | char * 356 | getsecret(const char *in) 357 | { 358 | size_t pos = 0; 359 | char *ret = malloc(pos+1); 360 | if (!ret) 361 | return NULL; 362 | 363 | if (!(in = findsecret (in))) 364 | return NULL; 365 | 366 | for(; *in != '&'; ++in) { 367 | char *tmp = ret; 368 | if (*in == ' ' || *in == '\t') 369 | continue; 370 | if (!(ret = realloc (ret, ++pos + 1))) { 371 | free (tmp); 372 | return NULL; 373 | } 374 | ret[pos-1] = *in; 375 | if (!*in) 376 | break; 377 | } 378 | ret[pos] = '\0'; 379 | return ret; 380 | } 381 | 382 | void 383 | usage (void) 384 | { 385 | fprintf (stderr, 386 | "usage: %s []\n" 387 | " %s - []\n", 388 | progname, progname); 389 | exit (1); 390 | } 391 | 392 | int 393 | main (int argc, char **argv) 394 | { 395 | char *keystr; 396 | void *key; 397 | size_t keylen; 398 | unsigned long val; 399 | uint32_t code; 400 | char line[256] = ""; 401 | 402 | progname = strrchr (argv[0], '/'); 403 | if (!progname) 404 | progname = argv[0]; 405 | else 406 | progname++; 407 | if (argc < 2 || argc > 3) 408 | usage (); 409 | 410 | if (strcmp (argv[1], "-")) 411 | keystr = argv[1]; 412 | else { 413 | int n; 414 | fgets (line, sizeof (line), stdin); 415 | n = strlen (line); 416 | if (n > 0 && line[n - 1] == '\n') 417 | line[n - 1] = '\0'; 418 | else if (n + 1 == sizeof (line)) { 419 | fprintf (stderr, "%s: input too long\n", progname); 420 | exit (1); 421 | } 422 | else if (n == 0) { 423 | /* If gpg or something fails on input. But allow null key with \n. */ 424 | fprintf (stderr, "%s: aborted\n", progname); 425 | exit (1); 426 | } 427 | keystr = line; 428 | } 429 | 430 | if (!(keystr = getsecret(keystr)) 431 | || !dearmor32a (&key, &keylen, keystr)) { 432 | fprintf (stderr, "%s: bad key\n", progname); 433 | exit (1); 434 | } 435 | 436 | if (argc == 3) 437 | val = strtoul (argv[2], NULL, 0); 438 | else 439 | val = (unsigned long) time (NULL) / 30; 440 | 441 | code = hotp (key, keylen, val); 442 | printf ("%06ld\n", (unsigned long) code % 1000000); 443 | 444 | return 0; 445 | } 446 | --------------------------------------------------------------------------------