Merge pull request #24 from danjurious/bitpay-12.1
wallet-utility: extract addresses and private keys
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -114,3 +114,4 @@ share/BitcoindComparisonTool.jar
|
||||
/doc/doxygen/
|
||||
|
||||
libbitcoinconsensus.pc
|
||||
wallet-utility
|
||||
|
||||
@@ -12,6 +12,7 @@ endif
|
||||
BITCOIND_BIN=$(top_builddir)/src/bitcoind$(EXEEXT)
|
||||
BITCOIN_QT_BIN=$(top_builddir)/src/qt/bitcoin-qt$(EXEEXT)
|
||||
BITCOIN_CLI_BIN=$(top_builddir)/src/bitcoin-cli$(EXEEXT)
|
||||
WALLET_UTILITY_BIN=$(top_builddir)/src/wallet-utility$(EXEEXT)
|
||||
BITCOIN_WIN_INSTALLER=$(PACKAGE)-$(PACKAGE_VERSION)-win$(WINDOWS_BITS)-setup$(EXEEXT)
|
||||
|
||||
OSX_APP=Bitcoin-Qt.app
|
||||
@@ -63,6 +64,7 @@ $(BITCOIN_WIN_INSTALLER): all-recursive
|
||||
STRIPPROG="$(STRIP)" $(INSTALL_STRIP_PROGRAM) $(BITCOIND_BIN) $(top_builddir)/release
|
||||
STRIPPROG="$(STRIP)" $(INSTALL_STRIP_PROGRAM) $(BITCOIN_QT_BIN) $(top_builddir)/release
|
||||
STRIPPROG="$(STRIP)" $(INSTALL_STRIP_PROGRAM) $(BITCOIN_CLI_BIN) $(top_builddir)/release
|
||||
STRIPPROG="$(STRIP)" $(INSTALL_STRIP_PROGRAM) $(WALLET_UTILITY_BIN) $(top_builddir)/release
|
||||
@test -f $(MAKENSIS) && $(MAKENSIS) -V2 $(top_builddir)/share/setup.nsi || \
|
||||
echo error: could not build $@
|
||||
@echo built $@
|
||||
@@ -145,6 +147,9 @@ $(BITCOIND_BIN): FORCE
|
||||
$(BITCOIN_CLI_BIN): FORCE
|
||||
$(MAKE) -C src $(@F)
|
||||
|
||||
$(WALLET_UTILITY_BIN): FORCE
|
||||
$(MAKE) -C src $(@F)
|
||||
|
||||
if USE_LCOV
|
||||
|
||||
baseline.info:
|
||||
|
||||
@@ -191,7 +191,7 @@ CPPFLAGS="$CPPFLAGS -DHAVE_BUILD_INFO -D__STDC_FORMAT_MACROS"
|
||||
|
||||
AC_ARG_WITH([utils],
|
||||
[AS_HELP_STRING([--with-utils],
|
||||
[build bitcoin-cli bitcoin-tx (default=yes)])],
|
||||
[build bitcoin-cli bitcoin-tx wallet-utility (default=yes)])],
|
||||
[build_bitcoin_utils=$withval],
|
||||
[build_bitcoin_utils=yes])
|
||||
|
||||
@@ -766,7 +766,7 @@ AC_MSG_CHECKING([whether to build bitcoind])
|
||||
AM_CONDITIONAL([BUILD_BITCOIND], [test x$build_bitcoind = xyes])
|
||||
AC_MSG_RESULT($build_bitcoind)
|
||||
|
||||
AC_MSG_CHECKING([whether to build utils (bitcoin-cli bitcoin-tx)])
|
||||
AC_MSG_CHECKING([whether to build utils (bitcoin-cli bitcoin-tx wallet-utility)])
|
||||
AM_CONDITIONAL([BUILD_BITCOIN_UTILS], [test x$build_bitcoin_utils = xyes])
|
||||
AC_MSG_RESULT($build_bitcoin_utils)
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ endif
|
||||
|
||||
if BUILD_BITCOIN_UTILS
|
||||
bin_PROGRAMS += bitcoin-cli bitcoin-tx
|
||||
if ENABLE_WALLET
|
||||
bin_PROGRAMS += wallet-utility
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: FORCE check-symbols check-security
|
||||
@@ -367,6 +370,14 @@ bitcoin_cli_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(EVENT_CFLAGS)
|
||||
bitcoin_cli_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
||||
bitcoin_cli_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
|
||||
|
||||
# wallet-utility binary #
|
||||
if ENABLE_WALLET
|
||||
wallet_utility_SOURCES = wallet-utility.cpp
|
||||
wallet_utility_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(EVENT_CFLAG)
|
||||
wallet_utility_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
||||
wallet_utility_LDFLAGS = $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS)
|
||||
endif
|
||||
|
||||
if TARGET_WINDOWS
|
||||
bitcoin_cli_SOURCES += bitcoin-cli-res.rc
|
||||
endif
|
||||
@@ -377,6 +388,10 @@ bitcoin_cli_LDADD = \
|
||||
$(LIBBITCOIN_UTIL)
|
||||
|
||||
bitcoin_cli_LDADD += $(BOOST_LIBS) $(SSL_LIBS) $(CRYPTO_LIBS) $(EVENT_LIBS)
|
||||
if ENABLE_WALLET
|
||||
wallet_utility_LDADD = libbitcoin_wallet.a $(LIBBITCOIN_COMMON) $(LIBBITCOIN_CRYPTO) $(LIBSECP256K1) $(LIBBITCOIN_UTIL) $(BOOST_LIBS) $(BDB_LIBS) $(CRYPTO_LIBS)
|
||||
endif
|
||||
|
||||
#
|
||||
|
||||
# bitcoin-tx binary #
|
||||
|
||||
@@ -17,7 +17,9 @@ EXTRA_DIST += \
|
||||
test/data/txcreate2.hex \
|
||||
test/data/txcreatedata1.hex \
|
||||
test/data/txcreatedata2.hex \
|
||||
test/data/txcreatesign.hex
|
||||
test/data/txcreatesign.hex \
|
||||
test/wallet-utility.py \
|
||||
test/data/wallet.dat
|
||||
|
||||
JSON_TEST_FILES = \
|
||||
test/data/script_valid.json \
|
||||
@@ -130,6 +132,10 @@ bitcoin_test_clean : FORCE
|
||||
check-local:
|
||||
@echo "Running test/bitcoin-util-test.py..."
|
||||
$(AM_V_at)srcdir=$(srcdir) PYTHONPATH=$(builddir)/test $(srcdir)/test/bitcoin-util-test.py
|
||||
if ENABLE_WALLET
|
||||
@echo "Running test/wallet-utility.py..."
|
||||
$(AM_V_at)srcdir=$(srcdir) PYTHONPATH=$(builddir)/test $(srcdir)/test/wallet-utility.py
|
||||
endif
|
||||
$(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C secp256k1 check
|
||||
$(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C univalue check
|
||||
|
||||
|
||||
BIN
src/test/data/wallet.dat
Normal file
BIN
src/test/data/wallet.dat
Normal file
Binary file not shown.
61
src/test/wallet-utility.py
Executable file
61
src/test/wallet-utility.py
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2014 BitPay, Inc.
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import buildenv
|
||||
import shutil
|
||||
|
||||
def assert_equal(thing1, thing2):
|
||||
if thing1 != thing2:
|
||||
raise AssertionError("%s != %s"%(str(thing1),str(thing2)))
|
||||
|
||||
if __name__ == '__main__':
|
||||
datadir = os.environ["srcdir"] + "/test/data"
|
||||
execprog = './wallet-utility' + buildenv.exeext
|
||||
execargs = '-datadir=' + datadir
|
||||
execrun = execprog + ' ' + execargs
|
||||
|
||||
proc = subprocess.Popen(execrun, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
try:
|
||||
outs = proc.communicate()
|
||||
except OSError:
|
||||
print("OSError, Failed to execute " + execprog)
|
||||
sys.exit(1)
|
||||
|
||||
output = json.loads(outs[0])
|
||||
|
||||
assert_equal(output[0], "13EngsxkRi7SJPPqCyJsKf34U8FoX9E9Av")
|
||||
assert_equal(output[1], "1FKCLGTpPeYBUqfNxktck8k5nqxB8sjim8")
|
||||
assert_equal(output[2], "13cdtE9tnNeXCZJ8KQ5WELgEmLSBLnr48F")
|
||||
|
||||
execargs = '-datadir=' + datadir + ' -dumppass'
|
||||
execrun = execprog + ' ' + execargs
|
||||
|
||||
proc = subprocess.Popen(execrun, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True)
|
||||
try:
|
||||
outs = proc.communicate()
|
||||
except OSError:
|
||||
print("OSError, Failed to execute " + execprog)
|
||||
sys.exit(1)
|
||||
|
||||
output = json.loads(outs[0])
|
||||
|
||||
assert_equal(output[0]['addr'], "13EngsxkRi7SJPPqCyJsKf34U8FoX9E9Av")
|
||||
assert_equal(output[0]['pkey'], "5Jz5BWE2WQxp1hGqDZeisQFV1mRFR2AVBAgiXCbNcZyXNjD9aUd")
|
||||
assert_equal(output[1]['addr'], "1FKCLGTpPeYBUqfNxktck8k5nqxB8sjim8")
|
||||
assert_equal(output[1]['pkey'], "5HsX2b3v2GjngYQ5ZM4mLp2b2apw6aMNVaPELV1YmpiYR1S4jzc")
|
||||
assert_equal(output[2]['addr'], "13cdtE9tnNeXCZJ8KQ5WELgEmLSBLnr48F")
|
||||
assert_equal(output[2]['pkey'], "5KCWAs1wX2ESiL4PfDR8XYVSSETHFd2jaRGxt1QdanBFTit4XcH")
|
||||
|
||||
if os.path.exists(datadir + '/database'):
|
||||
if os.path.isdir(datadir + '/database'):
|
||||
shutil.rmtree(datadir + '/database')
|
||||
|
||||
if os.path.exists(datadir + '/db.log'):
|
||||
os.remove(datadir + '/db.log')
|
||||
sys.exit(0)
|
||||
339
src/wallet-utility.cpp
Normal file
339
src/wallet-utility.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// Include local headers
|
||||
#include "wallet/walletdb.h"
|
||||
#include "util.h"
|
||||
#include "base58.h"
|
||||
#include "wallet/crypter.h"
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
|
||||
void show_help()
|
||||
{
|
||||
std::cout <<
|
||||
"This program outputs Bitcoin addresses and private keys from a wallet.dat file" << std::endl
|
||||
<< std::endl
|
||||
<< "Usage and options: "
|
||||
<< std::endl
|
||||
<< " -datadir=<directory> to tell the program where your wallet is"
|
||||
<< std::endl
|
||||
<< " -wallet=<name> (Optional) if your wallet is not named wallet.dat"
|
||||
<< std::endl
|
||||
<< " -regtest or -testnet (Optional) dumps addresses from regtest/testnet"
|
||||
<< std::endl
|
||||
<< " -dumppass (Optional)if you want to extract private keys associated with addresses"
|
||||
<< std::endl
|
||||
<< " -pass=<walletpassphrase> if you have encrypted private keys stored in your wallet"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
|
||||
class WalletUtilityDB : public CDB
|
||||
{
|
||||
private:
|
||||
typedef std::map<unsigned int, CMasterKey> MasterKeyMap;
|
||||
MasterKeyMap mapMasterKeys;
|
||||
unsigned int nMasterKeyMaxID;
|
||||
SecureString mPass;
|
||||
std::vector<CKeyingMaterial> vMKeys;
|
||||
|
||||
public:
|
||||
WalletUtilityDB(const std::string& strFilename, const char* pszMode = "r+", bool fFlushOnClose = true) : CDB(strFilename, pszMode, fFlushOnClose)
|
||||
{
|
||||
nMasterKeyMaxID = 0;
|
||||
mPass.reserve(100);
|
||||
}
|
||||
|
||||
std::string getAddress(CDataStream ssKey);
|
||||
std::string getKey(CDataStream ssKey, CDataStream ssValue);
|
||||
std::string getCryptedKey(CDataStream ssKey, CDataStream ssValue, std::string masterPass);
|
||||
bool updateMasterKeys(CDataStream ssKey, CDataStream ssValue);
|
||||
bool parseKeys(bool dumppriv, std::string masterPass);
|
||||
|
||||
bool DecryptSecret(const std::vector<unsigned char>& vchCiphertext, const uint256& nIV, CKeyingMaterial& vchPlaintext);
|
||||
bool Unlock();
|
||||
bool DecryptKey(const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Address from a public key in base58
|
||||
*/
|
||||
std::string WalletUtilityDB::getAddress(CDataStream ssKey)
|
||||
{
|
||||
CPubKey vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
CKeyID id = vchPubKey.GetID();
|
||||
std::string strAddr = CBitcoinAddress(id).ToString();
|
||||
|
||||
return strAddr;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Non encrypted private key in WIF
|
||||
*/
|
||||
std::string WalletUtilityDB::getKey(CDataStream ssKey, CDataStream ssValue)
|
||||
{
|
||||
std::string strKey;
|
||||
CPubKey vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
CPrivKey pkey;
|
||||
CKey key;
|
||||
|
||||
ssValue >> pkey;
|
||||
if (key.Load(pkey, vchPubKey, true))
|
||||
strKey = CBitcoinSecret(key).ToString();
|
||||
|
||||
return strKey;
|
||||
}
|
||||
|
||||
|
||||
bool WalletUtilityDB::DecryptSecret(const std::vector<unsigned char>& vchCiphertext, const uint256& nIV, CKeyingMaterial& vchPlaintext)
|
||||
{
|
||||
CCrypter cKeyCrypter;
|
||||
std::vector<unsigned char> chIV(WALLET_CRYPTO_KEY_SIZE);
|
||||
memcpy(&chIV[0], &nIV, WALLET_CRYPTO_KEY_SIZE);
|
||||
|
||||
BOOST_FOREACH(const CKeyingMaterial vMKey, vMKeys)
|
||||
{
|
||||
if(!cKeyCrypter.SetKey(vMKey, chIV))
|
||||
continue;
|
||||
if (cKeyCrypter.Decrypt(vchCiphertext, *((CKeyingMaterial*)&vchPlaintext)))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool WalletUtilityDB::Unlock()
|
||||
{
|
||||
CCrypter crypter;
|
||||
CKeyingMaterial vMasterKey;
|
||||
|
||||
BOOST_FOREACH(const MasterKeyMap::value_type& pMasterKey, mapMasterKeys)
|
||||
{
|
||||
if(!crypter.SetKeyFromPassphrase(mPass, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
|
||||
return false;
|
||||
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
|
||||
continue; // try another master key
|
||||
vMKeys.push_back(vMasterKey);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool WalletUtilityDB::DecryptKey(const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
|
||||
{
|
||||
CKeyingMaterial vchSecret;
|
||||
if(!DecryptSecret(vchCryptedSecret, vchPubKey.GetHash(), vchSecret))
|
||||
return false;
|
||||
|
||||
if (vchSecret.size() != 32)
|
||||
return false;
|
||||
|
||||
key.Set(vchSecret.begin(), vchSecret.end(), vchPubKey.IsCompressed());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Encrypted private key in WIF format
|
||||
*/
|
||||
std::string WalletUtilityDB::getCryptedKey(CDataStream ssKey, CDataStream ssValue, std::string masterPass)
|
||||
{
|
||||
mPass = masterPass.c_str();
|
||||
CPubKey vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
CKey key;
|
||||
|
||||
std::vector<unsigned char> vKey;
|
||||
ssValue >> vKey;
|
||||
|
||||
if (!Unlock())
|
||||
return "";
|
||||
|
||||
if(!DecryptKey(vKey, vchPubKey, key))
|
||||
return "";
|
||||
|
||||
std::string strKey = CBitcoinSecret(key).ToString();
|
||||
return strKey;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Master key derivation
|
||||
*/
|
||||
bool WalletUtilityDB::updateMasterKeys(CDataStream ssKey, CDataStream ssValue)
|
||||
{
|
||||
unsigned int nID;
|
||||
ssKey >> nID;
|
||||
CMasterKey kMasterKey;
|
||||
ssValue >> kMasterKey;
|
||||
if (mapMasterKeys.count(nID) != 0)
|
||||
{
|
||||
std::cout << "Error reading wallet database: duplicate CMasterKey id " << nID << std::endl;
|
||||
return false;
|
||||
}
|
||||
mapMasterKeys[nID] = kMasterKey;
|
||||
|
||||
if (nMasterKeyMaxID < nID)
|
||||
nMasterKeyMaxID = nID;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Look at all the records and parse keys for addresses and private keys
|
||||
*/
|
||||
bool WalletUtilityDB::parseKeys(bool dumppriv, std::string masterPass)
|
||||
{
|
||||
DBErrors result = DB_LOAD_OK;
|
||||
std::string strType;
|
||||
bool first = true;
|
||||
|
||||
try {
|
||||
Dbc* pcursor = GetCursor();
|
||||
if (!pcursor)
|
||||
{
|
||||
LogPrintf("Error getting wallet database cursor\n");
|
||||
result = DB_CORRUPT;
|
||||
}
|
||||
|
||||
if (dumppriv)
|
||||
{
|
||||
while (result == DB_LOAD_OK && true)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
int result = ReadAtCursor(pcursor, ssKey, ssValue);
|
||||
|
||||
if (result == DB_NOTFOUND) {
|
||||
break;
|
||||
}
|
||||
else if (result != 0)
|
||||
{
|
||||
LogPrintf("Error reading next record from wallet database\n");
|
||||
result = DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
|
||||
ssKey >> strType;
|
||||
if (strType == "mkey")
|
||||
{
|
||||
updateMasterKeys(ssKey, ssValue);
|
||||
}
|
||||
}
|
||||
pcursor->close();
|
||||
pcursor = GetCursor();
|
||||
}
|
||||
|
||||
while (result == DB_LOAD_OK && true)
|
||||
{
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
|
||||
|
||||
if (ret == DB_NOTFOUND)
|
||||
{
|
||||
std::cout << " ]" << std::endl;
|
||||
first = true;
|
||||
break;
|
||||
}
|
||||
else if (ret != DB_LOAD_OK)
|
||||
{
|
||||
LogPrintf("Error reading next record from wallet database\n");
|
||||
result = DB_CORRUPT;
|
||||
break;
|
||||
}
|
||||
|
||||
ssKey >> strType;
|
||||
|
||||
if (strType == "key" || strType == "ckey")
|
||||
{
|
||||
std::string strAddr = getAddress(ssKey);
|
||||
std::string strKey = "";
|
||||
|
||||
|
||||
if (dumppriv && strType == "key")
|
||||
strKey = getKey(ssKey, ssValue);
|
||||
if (dumppriv && strType == "ckey")
|
||||
{
|
||||
if (masterPass == "")
|
||||
{
|
||||
std::cout << "Encrypted wallet, please provide a password. See help below" << std::endl;
|
||||
show_help();
|
||||
result = DB_LOAD_FAIL;
|
||||
break;
|
||||
}
|
||||
strKey = getCryptedKey(ssKey, ssValue, masterPass);
|
||||
}
|
||||
|
||||
if (strAddr != "")
|
||||
{
|
||||
if (first)
|
||||
std::cout << "[ ";
|
||||
else
|
||||
std::cout << ", ";
|
||||
}
|
||||
|
||||
if (dumppriv)
|
||||
{
|
||||
std::cout << "{\"addr\" : \"" + strAddr + "\", "
|
||||
<< "\"pkey\" : \"" + strKey + "\"}"
|
||||
<< std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "\"" + strAddr + "\"";
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
pcursor->close();
|
||||
} catch (DbException &e) {
|
||||
std::cout << "DBException caught " << e.get_errno() << std::endl;
|
||||
} catch (std::exception &e) {
|
||||
std::cout << "Exception caught " << std::endl;
|
||||
}
|
||||
|
||||
if (result == DB_LOAD_OK)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
ParseParameters(argc, argv);
|
||||
std::string walletFile = GetArg("-wallet", "wallet.dat");
|
||||
std::string masterPass = GetArg("-pass", "");
|
||||
bool fDumpPass = GetBoolArg("-dumppass", false);
|
||||
bool help = GetBoolArg("-h", false);
|
||||
bool result = false;
|
||||
|
||||
if (help)
|
||||
{
|
||||
show_help();
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
SelectParams(ChainNameFromCommandLine());
|
||||
result = WalletUtilityDB(walletFile, "r").parseKeys(fDumpPass, masterPass);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cout << "Error opening wallet file " << walletFile << std::endl;
|
||||
std::cout << e.what() << std::endl;
|
||||
}
|
||||
|
||||
if (result)
|
||||
return 0;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
Reference in New Issue
Block a user