diff --git a/Makefile.am b/Makefile.am --- a/Makefile.am +++ b/Makefile.am @@ -262,7 +262,8 @@ test/util/data/txcreatescript2.json \ test/util/data/txcreatesignv1.hex \ test/util/data/txcreatesignv1.json \ - test/util/data/txcreatesignv2.hex + test/util/data/txcreatesignv2.hex \ + test/util/rpcauth-test.py CLEANFILES = $(OSX_DMG) $(BITCOIN_WIN_INSTALLER) diff --git a/configure.ac b/configure.ac --- a/configure.ac +++ b/configure.ac @@ -1222,6 +1222,7 @@ AC_CONFIG_FILES([src/config/version.h]) AC_CONFIG_LINKS([test/functional/test_runner.py:test/functional/test_runner.py]) AC_CONFIG_LINKS([test/util/bitcoin-util-test.py:test/util/bitcoin-util-test.py]) +AC_CONFIG_LINKS([test/util/rpcauth-test.py:test/util/rpcauth-test.py]) dnl boost's m4 checks do something really nasty: they export these vars. As a dnl result, they leak into secp256k1's configure and crazy things happen. diff --git a/share/rpcauth/rpcauth.py b/share/rpcauth/rpcauth.py --- a/share/rpcauth/rpcauth.py +++ b/share/rpcauth/rpcauth.py @@ -3,32 +3,47 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -import sys -import os -from random import SystemRandom import base64 import hmac +import os +from random import SystemRandom +import sys + + +def generate_salt(): + # This uses os.urandom() underneath + cryptogen = SystemRandom() + + # Create 16 byte hex salt + salt_sequence = [cryptogen.randrange(256) for _ in range(16)] + return ''.join([format(r, 'x') for r in salt_sequence]) + + +def generate_password(salt): + """Create 32 byte b64 password""" + password = base64.urlsafe_b64encode(os.urandom(32)).decode('utf-8') + + m = hmac.new(bytearray(salt, 'utf-8'), + bytearray(password, 'utf-8'), 'SHA256') + password_hmac = m.hexdigest() + + return password, password_hmac -if len(sys.argv) < 2: - sys.stderr.write('Please include username as an argument.\n') - sys.exit(0) -username = sys.argv[1] +def main(): + if len(sys.argv) < 2: + sys.stderr.write('Please include username as an argument.\n') + sys.exit(0) -# This uses os.urandom() underneath -cryptogen = SystemRandom() + username = sys.argv[1] -# Create 16 byte hex salt -salt_sequence = [cryptogen.randrange(256) for i in range(16)] -hexseq = list(map(hex, salt_sequence)) -salt = "".join([x[2:] for x in hexseq]) + salt = generate_salt() + password, password_hmac = generate_password(salt) -# Create 32 byte b64 password -password = base64.urlsafe_b64encode(os.urandom(32)).decode("utf-8") + print('String to be appended to bitcoin.conf:') + print('rpcauth={0}:{1}${2}'.format(username, salt, password_hmac)) + print('Your password:\n{0}'.format(password)) -m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), "SHA256") -result = m.hexdigest() -print("String to be appended to bitcoin.conf:") -print("rpcauth=" + username + ":" + salt + "$" + result) -print("Your password:\n" + password) +if __name__ == '__main__': + main() diff --git a/src/Makefile.test.include b/src/Makefile.test.include --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -201,6 +201,8 @@ check-local: $(BITCOIN_TESTS:.cpp=.cpp.test) @echo "Running test/util/bitcoin-util-test.py..." $(top_builddir)/test/util/bitcoin-util-test.py + @echo "Running test/util/rpcauth-test.py..." + $(top_builddir)/test/util/rpcauth-test.py $(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C secp256k1 check if EMBEDDED_UNIVALUE $(AM_V_at)$(MAKE) $(AM_MAKEFLAGS) -C univalue check diff --git a/test/util/rpcauth-test.py b/test/util/rpcauth-test.py new file mode 100755 --- /dev/null +++ b/test/util/rpcauth-test.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2018 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test share/rpcauth/rpcauth.py +""" +import base64 +import configparser +import hmac +import importlib +import os +import sys +import unittest + + +class TestRPCAuth(unittest.TestCase): + def setUp(self): + config = configparser.ConfigParser() + config_path = os.path.abspath( + os.path.join(os.sep, os.path.abspath(os.path.dirname(__file__)), + "../config.ini")) + with open(config_path) as config_file: + config.read_file(config_file) + sys.path.insert(0, os.path.dirname(config['environment']['RPCAUTH'])) + self.rpcauth = importlib.import_module('rpcauth') + + def test_generate_salt(self): + self.assertLessEqual(len(self.rpcauth.generate_salt()), 32) + self.assertGreaterEqual(len(self.rpcauth.generate_salt()), 16) + + def test_generate_password(self): + salt = self.rpcauth.generate_salt() + password, password_hmac = self.rpcauth.generate_password(salt) + + expected_password = base64.urlsafe_b64encode( + base64.urlsafe_b64decode(password)).decode('utf-8') + self.assertEqual(expected_password, password) + + def test_check_password_hmac(self): + salt = self.rpcauth.generate_salt() + password, password_hmac = self.rpcauth.generate_password(salt) + + m = hmac.new(bytearray(salt, 'utf-8'), + bytearray(password, 'utf-8'), 'SHA256') + expected_password_hmac = m.hexdigest() + + self.assertEqual(expected_password_hmac, password_hmac) + + +if __name__ == '__main__': + unittest.main()