26 changed files with 1630 additions and 268 deletions
			
			
		@ -0,0 +1,121 @@
					 | 
				
			||||
<?php | 
				
			||||
# | 
				
			||||
function set_2fa($data, $user, $tfa, $ans, $err) | 
				
			||||
{ | 
				
			||||
 $pg = '<h1>Two Factor Authentication Settings</h1>'; | 
				
			||||
 | 
				
			||||
 if ($err !== null and $err != '') | 
				
			||||
	$pg .= "<span class=err>$err<br><br></span>"; | 
				
			||||
 | 
				
			||||
 $pg .= '<table cellpadding=20 cellspacing=0 border=1>'; | 
				
			||||
 $pg .= '<tr class=dc><td><center>'; | 
				
			||||
 | 
				
			||||
 $pg .= makeForm('2fa'); | 
				
			||||
 $pg .= '<table cellpadding=5 cellspacing=0 border=0>'; | 
				
			||||
 $pg .= '<tr class=dc><td>'; | 
				
			||||
 switch ($tfa) | 
				
			||||
 { | 
				
			||||
  case '': | 
				
			||||
	$pg .= '<tr class=dl><td>'; | 
				
			||||
	$pg .= "You don't have 2FA setup yet<br><br>"; | 
				
			||||
	$pg .= 'To use 2FA you need an App on your phone/tablet<br>'; | 
				
			||||
	$pg .= 'The free and recommended ones that have been tested here are:<br><br>'; | 
				
			||||
	$pg .= "Android: Google Play 'FreeOTP Authenticator' by Red Hat<br>"; | 
				
			||||
	$pg .= "Apple: App Store 'OTP Auth' by Roland Moers<br><br>"; | 
				
			||||
	$pg .= 'Click here to start setting up 2FA: '; | 
				
			||||
	$pg .= '<input type=submit name=Setup value=Setup>'; | 
				
			||||
	$pg .= '</td></tr>'; | 
				
			||||
	break; | 
				
			||||
  case 'test': | 
				
			||||
	$pg .= '<tr class=dc><td>'; | 
				
			||||
	$pg .= '2FA is not yet enabled.<br>'; | 
				
			||||
	$pg .= 'Your 2FA key has been created but needs testing.<br><br>'; | 
				
			||||
	if (isset($ans['2fa_key'])) | 
				
			||||
	{ | 
				
			||||
		$key = $ans['2fa_key']; | 
				
			||||
		$sfainfo = $ans['2fa_issuer'].': '.$ans['2fa_auth'].' '. | 
				
			||||
			   $ans['2fa_hash'].' '.$ans['2fa_time'].'s'; | 
				
			||||
		$who = substr($user, 0, 8); | 
				
			||||
		$sfaurl = 'otpauth://'.$ans['2fa_auth'].'/'.$ans['2fa_issuer']. | 
				
			||||
			  ':'.htmlspecialchars($who).'?secret='.$ans['2fa_key']. | 
				
			||||
			  '&algorithm='.$ans['2fa_hash'].'&issuer='.$ans['2fa_issuer']; | 
				
			||||
	} | 
				
			||||
	else | 
				
			||||
	{ | 
				
			||||
		$key = 'unavailable'; | 
				
			||||
		$sfainfo = 'unavailable'; | 
				
			||||
		$sfaurl = 'unavailable'; | 
				
			||||
	} | 
				
			||||
	$pg .= "Your 2FA Secret Key is: $key<br>"; | 
				
			||||
	$pg .= "2FA Settings are $sfainfo<br><br>"; | 
				
			||||
	$pg .= "2FA URL is <a href='$sfaurl'>Click</a><br><br>"; | 
				
			||||
	$pg .= '2FA Value: <input name=Value value="" size=10> '; | 
				
			||||
	$pg .= '<input type=submit name=Test value=Test>'; | 
				
			||||
	$pg .= '</td></tr>'; | 
				
			||||
	break; | 
				
			||||
  case 'ok': | 
				
			||||
	$pg .= '<tr class=dc><td>'; | 
				
			||||
	$pg .= '2FA is enabled on your account.<br><br>'; | 
				
			||||
	$pg .= 'If you wish to replace your Secret Key with a new one:<br><br>'; | 
				
			||||
	$pg .= 'Current 2FA Value: <input name=Value value="" size=10> '; | 
				
			||||
	$pg .= '<input type=submit name=New value=New><span class=st1>*</span><br><br>'; | 
				
			||||
	$pg .= '<span class=st1>*</span>WARNING: replacing the Secret Key will disable 2FA<br>'; | 
				
			||||
	$pg .= 'until you successfully test the new key.<br><br>'; | 
				
			||||
	$pg .= '</td></tr>'; | 
				
			||||
	break; | 
				
			||||
 } | 
				
			||||
 | 
				
			||||
 $pg .= '</table></form>'; | 
				
			||||
 | 
				
			||||
 $pg .= '</center></td></tr>'; | 
				
			||||
 $pg .= '</table>'; | 
				
			||||
 | 
				
			||||
 return $pg; | 
				
			||||
} | 
				
			||||
# | 
				
			||||
function do2fa($data, $user) | 
				
			||||
{ | 
				
			||||
 $err = ''; | 
				
			||||
 $setup = getparam('Setup', false); | 
				
			||||
 if ($setup === 'Setup') | 
				
			||||
 { | 
				
			||||
	// rand() included as part of the entropy | 
				
			||||
	$ans = get2fa($user, 'setup', rand(1073741824,2147483647), 0); | 
				
			||||
 } | 
				
			||||
 else | 
				
			||||
 { | 
				
			||||
	$value = getparam('Value', false); | 
				
			||||
 | 
				
			||||
	$test = getparam('Test', false); | 
				
			||||
	if ($test === 'Test' and $value !== null) | 
				
			||||
		$ans = get2fa($user, 'test', 0, $value); | 
				
			||||
	else | 
				
			||||
	{ | 
				
			||||
		$nw = getparam('New', false); | 
				
			||||
		if ($nw === 'New' and $value !== null) | 
				
			||||
			$ans = get2fa($user, 'new', rand(1073741824,2147483647), $value); | 
				
			||||
		else | 
				
			||||
			$ans = get2fa($user, '', 0, 0); | 
				
			||||
	} | 
				
			||||
 } | 
				
			||||
 if ($ans['STATUS'] != 'ok') | 
				
			||||
	$err = 'DBERR'; | 
				
			||||
 else | 
				
			||||
 { | 
				
			||||
	if (isset($ans['2fa_error'])) | 
				
			||||
		$err = $ans['2fa_error']; | 
				
			||||
 } | 
				
			||||
 if (!isset($ans['2fa_status'])) | 
				
			||||
	$tfa = null; | 
				
			||||
 else | 
				
			||||
	$tfa = $ans['2fa_status']; | 
				
			||||
 $pg = set_2fa($data, $user, $tfa, $ans, $err); | 
				
			||||
 return $pg; | 
				
			||||
} | 
				
			||||
# | 
				
			||||
function show_2fa($info, $page, $menu, $name, $user) | 
				
			||||
{ | 
				
			||||
 gopage($info, NULL, 'do2fa', $page, $menu, $name, $user); | 
				
			||||
} | 
				
			||||
# | 
				
			||||
?> | 
				
			||||
@ -0,0 +1,38 @@
					 | 
				
			||||
SET SESSION AUTHORIZATION 'postgres'; | 
				
			||||
 | 
				
			||||
BEGIN transaction; | 
				
			||||
 | 
				
			||||
DO $$ | 
				
			||||
DECLARE ver TEXT; | 
				
			||||
BEGIN | 
				
			||||
 | 
				
			||||
 UPDATE version set version='1.0.1' where vlock=1 and version='1.0.0'; | 
				
			||||
 | 
				
			||||
 IF found THEN | 
				
			||||
  RETURN; | 
				
			||||
 END IF; | 
				
			||||
 | 
				
			||||
 SELECT version into ver from version | 
				
			||||
  WHERE vlock=1; | 
				
			||||
 | 
				
			||||
 RAISE EXCEPTION 'Wrong DB version - expect "1.0.0" - found "%"', ver; | 
				
			||||
 | 
				
			||||
END $$; | 
				
			||||
 | 
				
			||||
ALTER TABLE ONLY users | 
				
			||||
  ADD COLUMN userdata text DEFAULT ''::text NOT NULL, | 
				
			||||
  ADD COLUMN userbits bigint NOT NULL DEFAULT 0; | 
				
			||||
 | 
				
			||||
ALTER TABLE ONLY users | 
				
			||||
  ALTER COLUMN userbits DROP DEFAULT; | 
				
			||||
 | 
				
			||||
-- match based on ckdb_data.c like_address() | 
				
			||||
UPDATE users set userbits=1 where username ~ '[13][A-HJ-NP-Za-km-z1-9]{15,}'; | 
				
			||||
 | 
				
			||||
ALTER TABLE ONLY workers | 
				
			||||
  ADD COLUMN workerbits bigint NOT NULL DEFAULT 0; | 
				
			||||
 | 
				
			||||
ALTER TABLE ONLY workers | 
				
			||||
  ALTER COLUMN workerbits DROP DEFAULT; | 
				
			||||
 | 
				
			||||
END transaction; | 
				
			||||
@ -0,0 +1,239 @@
					 | 
				
			||||
/*
 | 
				
			||||
 * Copyright 2015 Andrew Smith | 
				
			||||
 * | 
				
			||||
 * This program is free software; you can redistribute it and/or modify it | 
				
			||||
 * under the terms of the GNU General Public License as published by the Free | 
				
			||||
 * Software Foundation; either version 3 of the License, or (at your option) | 
				
			||||
 * any later version.  See COPYING for more details. | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
#include <openssl/x509.h> | 
				
			||||
#include <openssl/hmac.h> | 
				
			||||
#include "ckdb.h" | 
				
			||||
 | 
				
			||||
#if (SHA256SIZBIN != SHA256_DIGEST_LENGTH) | 
				
			||||
#error "SHA256SIZBIN must = OpenSSL SHA256_DIGEST_LENGTH" | 
				
			||||
#endif | 
				
			||||
 | 
				
			||||
static char b32code[] = { | 
				
			||||
	'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', | 
				
			||||
	'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', | 
				
			||||
	'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', | 
				
			||||
	'Y', 'Z', '2', '3', '4', '5', '6', '7' | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
#define ASSERT32(condition) __maybe_unused static char b32code_length_must_be_32[(condition)?1:-1] | 
				
			||||
ASSERT32(sizeof(b32code) == 32); | 
				
			||||
 | 
				
			||||
// bin is bigendian, return buf is bigendian
 | 
				
			||||
char *_tob32(USERS *users, unsigned char *bin, size_t len, char *name, | 
				
			||||
	     size_t olen, WHERE_FFL_ARGS) | 
				
			||||
{ | 
				
			||||
	size_t osiz = (len * 8 + 4) / 5; | 
				
			||||
	char *buf, *ptr, *st = NULL; | 
				
			||||
	int i, j, bits, ch; | 
				
			||||
 | 
				
			||||
	if (osiz != olen) { | 
				
			||||
		LOGEMERG("%s() of '%s' data for '%s' invalid olen=%d != osiz=%d" | 
				
			||||
			 WHERE_FFL, | 
				
			||||
			 __func__, name, safe_text_nonull(users->username), | 
				
			||||
			 (int)olen, (int)osiz, WHERE_FFL_PASS); | 
				
			||||
		FREENULL(st); | 
				
			||||
		olen = osiz; | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	buf = malloc(olen+1); | 
				
			||||
	ptr = buf + olen; | 
				
			||||
	*ptr = '\0'; | 
				
			||||
	i = 0; | 
				
			||||
	while (--ptr >= buf) { | 
				
			||||
		j = i / 8; | 
				
			||||
		bits = (31 << (i % 8)); | 
				
			||||
		ch = (bin[len-j-1] & bits) >> (i % 8); | 
				
			||||
		if (bits > 255) | 
				
			||||
			ch |= (bin[len-j-2] & (bits >> 8)) << (8 - (i % 8)); | 
				
			||||
		// Shouldn't ever happen
 | 
				
			||||
		if (ch < 0 || ch > 31) { | 
				
			||||
			char *binstr = bin2hex(bin, len); | 
				
			||||
			LOGEMERG("%s() failure of '%s' data for '%s' invalid " | 
				
			||||
				 "ch=%d, i=%d j=%d bits=%d bin=0x%s len=%d " | 
				
			||||
				 "olen=%d" WHERE_FFL, | 
				
			||||
				 __func__, name, | 
				
			||||
				 safe_text_nonull(users->username), ch, i, j, | 
				
			||||
				 bits, binstr, (int)len, (int)olen, | 
				
			||||
				 WHERE_FFL_PASS); | 
				
			||||
			FREENULL(st); | 
				
			||||
			FREENULL(binstr); | 
				
			||||
			ch = 0; | 
				
			||||
		} | 
				
			||||
		*ptr = b32code[ch]; | 
				
			||||
		i += 5; | 
				
			||||
	} | 
				
			||||
	return buf; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
bool gen_data(__maybe_unused USERS *users, unsigned char *buf, size_t len, | 
				
			||||
	      int32_t entropy) | 
				
			||||
{ | 
				
			||||
	unsigned char *ptr; | 
				
			||||
	ssize_t ret, want, got; | 
				
			||||
	int i; | 
				
			||||
 | 
				
			||||
	int fil = open("/dev/random", O_RDONLY); | 
				
			||||
	if (fil == -1) | 
				
			||||
		return false; | 
				
			||||
 | 
				
			||||
	want = (ssize_t)len; | 
				
			||||
	got = 0; | 
				
			||||
	while (got < want) { | 
				
			||||
		ret = read(fil, buf+got, want-got); | 
				
			||||
		if (ret < 0) { | 
				
			||||
			close(fil); | 
				
			||||
			return false; | 
				
			||||
		} | 
				
			||||
		got += ret; | 
				
			||||
	} | 
				
			||||
	close(fil); | 
				
			||||
 | 
				
			||||
	ptr = (unsigned char *)&entropy; | 
				
			||||
	for (i = 0; i < (int)sizeof(entropy) && (i + sizeof(entropy)) < len; i++) | 
				
			||||
		buf[i+sizeof(entropy)] ^= *(ptr + i); | 
				
			||||
 | 
				
			||||
	return true; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
K_ITEM *gen_2fa_key(K_ITEM *old_u_item, int32_t entropy, char *by, char *code, | 
				
			||||
		    char *inet, tv_t *cd,  K_TREE *trf_root) | 
				
			||||
{ | 
				
			||||
	unsigned char key[TOTPAUTH_KEYSIZE]; | 
				
			||||
	K_ITEM *u_item = NULL; | 
				
			||||
	USERS *old_users, *users; | 
				
			||||
	bool ok; | 
				
			||||
 | 
				
			||||
	DATA_USERS(old_users, old_u_item); | 
				
			||||
	ok = gen_data(old_users, key, sizeof(key), entropy); | 
				
			||||
	if (ok) { | 
				
			||||
		K_WLOCK(users_free); | 
				
			||||
		u_item = k_unlink_head(users_free); | 
				
			||||
		K_WUNLOCK(users_free); | 
				
			||||
		DATA_USERS(users, u_item); | 
				
			||||
		memcpy(users, old_users, sizeof(*users)); | 
				
			||||
		if (users->userdata != EMPTY) { | 
				
			||||
			users->userdata = strdup(users->userdata); | 
				
			||||
			if (!users->userdata) | 
				
			||||
				quithere(1, "strdup OOM"); | 
				
			||||
		} | 
				
			||||
		users_userdata_add_bin(users, USER_TOTPAUTH_NAME, | 
				
			||||
					USER_TOTPAUTH, key, sizeof(key)); | 
				
			||||
		users_userdata_add_txt(users, USER_TEST2FA_NAME, | 
				
			||||
					USER_TEST2FA, "Y"); | 
				
			||||
		ok = users_replace(NULL, u_item, old_u_item, by, code, inet, cd, | 
				
			||||
				   trf_root); | 
				
			||||
		if (!ok) { | 
				
			||||
			// u_item was cleaned up in user_replace()
 | 
				
			||||
			u_item = NULL; | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
	return u_item; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
bool check_2fa(USERS *users, int32_t value) | 
				
			||||
{ | 
				
			||||
	char *st = NULL, *tmp1 = NULL, *tmp2 = NULL, *tmp3 = NULL; | 
				
			||||
	unsigned char tim[sizeof(int64_t)], *bin, *hash; | 
				
			||||
	unsigned int reslen; | 
				
			||||
	size_t binlen; | 
				
			||||
	HMAC_CTX ctx; | 
				
			||||
	int64_t now; | 
				
			||||
	int32_t otp; | 
				
			||||
	int i, offset; | 
				
			||||
 | 
				
			||||
	now = (int64_t)time(NULL) / TOTPAUTH_TIME; | 
				
			||||
	bin = users_userdata_get_bin(users, USER_TOTPAUTH_NAME, | 
				
			||||
				     USER_TOTPAUTH, &binlen); | 
				
			||||
	if (binlen != TOTPAUTH_KEYSIZE) { | 
				
			||||
		LOGERR("%s() invalid key for '%s/%s " | 
				
			||||
			"len(%d) != %d", | 
				
			||||
			__func__, | 
				
			||||
			st = safe_text_nonull(users->username), | 
				
			||||
			USER_TOTPAUTH_NAME, (int)binlen, | 
				
			||||
			TOTPAUTH_KEYSIZE); | 
				
			||||
		FREENULL(st); | 
				
			||||
		return false; | 
				
			||||
	} | 
				
			||||
	for (i = 0; i < (int)sizeof(int64_t); i++) | 
				
			||||
		tim[i] = (now >> 8 * ((sizeof(int64_t) - 1) - i)) & 0xff; | 
				
			||||
 | 
				
			||||
	LOGDEBUG("%s() '%s/%s tim=%"PRId64"=%s key=%s=%s", __func__, | 
				
			||||
		 st = safe_text_nonull(users->username), | 
				
			||||
		 USER_TOTPAUTH_NAME, now, | 
				
			||||
		 tmp1 = (char *)bin2hex(&tim, sizeof(tim)), | 
				
			||||
		 tmp2 = (char *)bin2hex(bin, TOTPAUTH_KEYSIZE), | 
				
			||||
		 tmp3 = tob32(users, bin, binlen,USER_TOTPAUTH_NAME, TOTPAUTH_DSP_KEYSIZE)); | 
				
			||||
	FREENULL(tmp3); | 
				
			||||
	FREENULL(tmp2); | 
				
			||||
	FREENULL(tmp1); | 
				
			||||
	FREENULL(st); | 
				
			||||
 | 
				
			||||
	hash = malloc(SHA256_DIGEST_LENGTH); | 
				
			||||
	if (!hash) | 
				
			||||
		quithere(1, "malloc OOM"); | 
				
			||||
 | 
				
			||||
	HMAC_CTX_init(&ctx); | 
				
			||||
	HMAC_Init_ex(&ctx, bin, binlen, EVP_sha256(), NULL); | 
				
			||||
	HMAC_Update(&ctx, (unsigned char *)&tim, sizeof(tim)); | 
				
			||||
	HMAC_Final(&ctx, hash, &reslen); | 
				
			||||
 | 
				
			||||
	LOGDEBUG("%s() '%s/%s hash=%s", __func__, | 
				
			||||
		 st = safe_text_nonull(users->username), | 
				
			||||
		 USER_TOTPAUTH_NAME, | 
				
			||||
		 tmp1 = (char *)bin2hex(hash, SHA256_DIGEST_LENGTH)); | 
				
			||||
	FREENULL(tmp1); | 
				
			||||
	FREENULL(st); | 
				
			||||
 | 
				
			||||
	offset = hash[reslen-1] & 0xf; | 
				
			||||
 | 
				
			||||
	otp = ((hash[offset] & 0x7f) << 24) | ((hash[offset+1] & 0xff) << 16) | | 
				
			||||
	      ((hash[offset+2] & 0xff) << 8) | (hash[offset+3] & 0xff); | 
				
			||||
 | 
				
			||||
	otp %= 1000000; | 
				
			||||
 | 
				
			||||
	LOGDEBUG("%s() '%s/%s offset=%d otp=%"PRId32" value=%"PRId32, | 
				
			||||
		 __func__, st = safe_text_nonull(users->username), | 
				
			||||
		 USER_TOTPAUTH_NAME, offset, otp, value); | 
				
			||||
	FREENULL(st); | 
				
			||||
	FREENULL(hash); | 
				
			||||
 | 
				
			||||
	if (otp == value) | 
				
			||||
		return true; | 
				
			||||
	else | 
				
			||||
		return false; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
bool tst_2fa(K_ITEM *old_u_item, int32_t value, char *by, char *code, | 
				
			||||
	     char *inet, tv_t *cd, K_TREE *trf_root) | 
				
			||||
{ | 
				
			||||
	K_ITEM *u_item; | 
				
			||||
	USERS *old_users, *users; | 
				
			||||
	bool ok; | 
				
			||||
 | 
				
			||||
	DATA_USERS(old_users, old_u_item); | 
				
			||||
	ok = check_2fa(old_users, value); | 
				
			||||
	if (ok) { | 
				
			||||
		K_WLOCK(users_free); | 
				
			||||
		u_item = k_unlink_head(users_free); | 
				
			||||
		K_WUNLOCK(users_free); | 
				
			||||
		DATA_USERS(users, u_item); | 
				
			||||
		memcpy(users, old_users, sizeof(*users)); | 
				
			||||
		if (users->userdata != EMPTY) { | 
				
			||||
			users->userdata = strdup(users->userdata); | 
				
			||||
			if (!users->userdata) | 
				
			||||
				quithere(1, "strdup OOM"); | 
				
			||||
		} | 
				
			||||
		users_userdata_del(users, USER_TEST2FA_NAME, USER_TEST2FA); | 
				
			||||
		ok = users_replace(NULL, u_item, old_u_item, by, code, inet, cd, | 
				
			||||
				   trf_root); | 
				
			||||
		// if !ok : u_item was cleaned up in user_replace()
 | 
				
			||||
	} | 
				
			||||
	return ok; | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue