Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-user support #172

Merged
merged 11 commits into from
Jun 27, 2020
2 changes: 1 addition & 1 deletion src/cryptoadvance/specter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from .bitcoind import (BitcoindDockerController,
fetch_wallet_addresses_for_mining)
from .helpers import load_jsons, which
from .helpers import which
from .server import DATA_FOLDER, create_app, init_app

from os import path
Expand Down
192 changes: 142 additions & 50 deletions src/cryptoadvance/specter/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
from flask_login.config import EXEMPT_METHODS


from .helpers import get_devices_with_keys_by_type, run_shell, set_loglevel, get_loglevel, get_version_info
from .helpers import alias, get_devices_with_keys_by_type, hash_password, get_loglevel, get_version_info, run_shell, set_loglevel, verify_password
from .specter import Specter
from .specter_error import SpecterError
from .wallet_manager import purposes
from .rpc import RpcError
from .user import User
from datetime import datetime
import urllib

Expand Down Expand Up @@ -99,36 +100,85 @@ def index():
def login():
''' login '''
app.specter.check()
if request.method == 'POST':
# ToDo: check the password via RPC-call
if app.specter.cli is None:
flash("We could not check your password, maybe Bitcoin Core is not running or not configured?","error")
app.logger.info("AUDIT: Failed to check password")
return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401
cli = app.specter.cli.clone()
cli.passwd = request.form['password']
if cli.test_connection():
app.login()
app.logger.info("AUDIT: Successfull Login via RPC-credentials")
flash('Logged in successfully.',"info")
if request.form.get('next') and request.form.get('next').startswith("http"):
response = redirect(request.form['next'])
else:
response = redirect(url_for('index'))
return response
else:
flash('Invalid username or password', "error")
app.logger.info("AUDIT: Invalid password login attempt")
return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401
if request.method == 'POST':
if app.specter.config['auth'] == 'none':
app.login('admin')
app.logger.info("AUDIT: Successfull Login no credentials")
return redirect_login(request)
if app.specter.config['auth'] == 'rpcpasswordaspin':
# TODO: check the password via RPC-call
if app.specter.cli is None:
flash("We could not check your password, maybe Bitcoin Core is not running or not configured?","error")
app.logger.info("AUDIT: Failed to check password")
return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401
cli = app.specter.cli.clone()
cli.passwd = request.form['password']
if cli.test_connection():
app.login('admin')
app.logger.info("AUDIT: Successfull Login via RPC-credentials")
return redirect_login(request)
elif app.specter.config['auth'] == 'usernamepassword':
# TODO: This way both "User" and "user" will pass as usernames, should there be strict check on that here? Or should we keep it like this?
username = request.form['username']
password = request.form['password']
user = User.get_user_by_name(app.specter, username)
if user:
if verify_password(user.password, password):
app.login(user.id)
return redirect_login(request)
# Either invalid method or incorrect credentials
flash('Invalid username or password', "error")
app.logger.info("AUDIT: Invalid password login attempt")
return render_template('login.jinja', specter=app.specter, data={'controller':'controller.login'}), 401
else:
if app.config.get('LOGIN_DISABLED'):
app.login('admin')
return redirect('/')
return render_template('login.jinja', specter=app.specter, data={'next':request.args.get('next')})

def redirect_login(request):
flash('Logged in successfully.',"info")
if request.form.get('next') and request.form.get('next').startswith("http"):
response = redirect(request.form['next'])
else:
response = redirect(url_for('index'))
return response

@app.route('/register', methods=['GET', 'POST'])
def register():
''' register '''
app.specter.check()
if request.method == 'POST':
username = request.form['username']
password = hash_password(request.form['password'])
otp = request.form['otp']
user_id = alias(username)
if User.get_user(app.specter, user_id) or User.get_user_by_name(app.specter, username):
flash('Username is already taken, please choose another one', "error")
return redirect('/register?otp={}'.format(otp))
if app.specter.burn_new_user_otp(otp):
config = {
"explorers": {
"main": "",
"test": "",
"regtest": "",
"signet": "",
},
"hwi_bridge_url": "/hwi/api/",
}
user = User(user_id, username, password, config)
user.save_info(app.specter)
return redirect('/login')
else:
flash('Invalid registration link, please request a new link from the node operator.', 'error')
return redirect('/register?otp={}'.format(otp))
return render_template('register.jinja', specter=app.specter)

@app.route('/logout', methods=['GET', 'POST'])
def logout():
logout_user()
flash('You were logged out',"info")
app.specter.clear_user_session()
return redirect("/login")

@app.route('/settings/', methods=['GET', 'POST'])
Expand All @@ -143,22 +193,33 @@ def settings():
host = rpc['host']
protocol = 'http'
explorer = app.specter.explorer
auth = app.specter.config["auth"]
hwi_bridge_url = app.specter.config['hwi_bridge_url']
auth = app.specter.config['auth']
stepansnigirev marked this conversation as resolved.
Show resolved Hide resolved
if auth == 'none':
app.login('admin')
hwi_bridge_url = app.specter.hwi_bridge_url
new_otp = -1
loglevel = get_loglevel(app)
if "protocol" in rpc:
protocol = rpc["protocol"]
test = None
if request.method == 'POST':
user = request.form['username']
passwd = request.form['password']
port = request.form['port']
host = request.form['host']
explorer = request.form["explorer"]
auth = request.form['auth']
loglevel = request.form["loglevel"]
hwi_bridge_url = request.form['hwi_bridge_url']
action = request.form['action']
explorer = request.form['explorer']
hwi_bridge_url = request.form['hwi_bridge_url']
if 'specter_username' in request.form:
specter_username = request.form['specter_username']
specter_password = request.form['specter_password']
else:
specter_username = None
specter_password = None
if current_user.is_admin:
stepansnigirev marked this conversation as resolved.
Show resolved Hide resolved
user = request.form['username']
passwd = request.form['password']
port = request.form['port']
host = request.form['host']
auth = request.form['auth']
loglevel = request.form['loglevel']

# protocol://host
if "://" in host:
arr = host.split("://")
Expand All @@ -173,26 +234,56 @@ def settings():
protocol=protocol,
autodetect=False
)
if action == "save":
app.specter.update_rpc( user=user,
password=passwd,
port=port,
host=host,
protocol=protocol,
autodetect=False
)
app.specter.update_explorer(explorer)
app.specter.update_auth(auth)
app.specter.update_hwi_bridge_url(hwi_bridge_url)
if auth == "rpcpasswordaspin":
app.config['LOGIN_DISABLED'] = False
else:
app.config['LOGIN_DISABLED'] = True
set_loglevel(app,loglevel)
elif action == "save":
if specter_username:
if current_user.username != specter_username:
if User.get_user_by_name(app.specter, specter_username):
flash('Username is already taken, please choose another one', "error")
return render_template("settings.jinja",
test=test,
username=user,
password=passwd,
port=port,
host=host,
protocol=protocol,
explorer=explorer,
auth=auth,
hwi_bridge_url=hwi_bridge_url,
new_otp=new_otp,
loglevel=loglevel,
specter=app.specter,
current_version=current_version,
rand=rand)
current_user.username = specter_username
if specter_password:
current_user.password = hash_password(specter_password)
current_user.save_info(app.specter)
if current_user.is_admin:
stepansnigirev marked this conversation as resolved.
Show resolved Hide resolved
app.specter.update_rpc( user=user,
password=passwd,
port=port,
host=host,
protocol=protocol,
autodetect=False
)
app.specter.update_auth(auth)
if auth == "rpcpasswordaspin" or auth == "usernamepassword":
app.config['LOGIN_DISABLED'] = False
else:
app.config['LOGIN_DISABLED'] = True
set_loglevel(app,loglevel)

app.specter.update_explorer(explorer, current_user)
app.specter.update_hwi_bridge_url(hwi_bridge_url, current_user)
app.specter.check()
return redirect("/")
else:
pass
elif action == "adduser":
if current_user.is_admin:
new_otp = random.randint(100000, 999999)
app.specter.add_new_user_otp({ 'otp': new_otp, 'created_at': time.time() })
flash('New user link generated successfully: {}register?otp={}'.format(request.url_root, new_otp), 'info')
else:
flash('Error: Only the admin account can issue new registration links.', 'error')
return render_template("settings.jinja",
test=test,
username=user,
Expand All @@ -203,6 +294,7 @@ def settings():
explorer=explorer,
auth=auth,
hwi_bridge_url=hwi_bridge_url,
new_otp=new_otp,
loglevel=loglevel,
specter=app.specter,
current_version=current_version,
Expand Down
6 changes: 3 additions & 3 deletions src/cryptoadvance/specter/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ class DeviceManager:
'''
# of them via json-files in an empty data folder
def __init__(self, data_folder):
self.update(data_folder=data_folder)

def update(self, data_folder=None):
if data_folder is not None:
self.data_folder = data_folder
if data_folder.startswith("~"):
data_folder = os.path.expanduser(data_folder)
# creating folders if they don't exist
if not os.path.isdir(data_folder):
os.mkdir(data_folder)
self.update()

def update(self):
self.devices = {}
devices_files = load_jsons(self.data_folder, key="name")
for device_alias in devices_files:
Expand Down
42 changes: 40 additions & 2 deletions src/cryptoadvance/specter/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import collections, copy, hashlib, json, logging, os, six, subprocess, sys
import binascii, collections, copy, hashlib, json, logging, os, six, subprocess, sys
from collections import OrderedDict
from .descriptor import AddChecksum

Expand Down Expand Up @@ -178,12 +178,36 @@ def get_version_info():

return current_version, latest_version, latest_version != current_version

def get_users_json(specter):
users = [
{
'id': 'admin',
'username': 'admin',
'password': hash_password('admin'),
'is_admin': True
}
]


# if users.json file exists - load from it
if os.path.isfile(os.path.join(specter.data_folder, "users.json")):
with open(os.path.join(specter.data_folder, "users.json"), "r") as f:
users = json.loads(f.read())
# otherwise - create one and assign unique id
else:
save_users_json(specter, users)
return users

def save_users_json(specter, users):
with open(os.path.join(specter.data_folder, 'users.json'), "w") as f:
f.write(json.dumps(users, indent=4))

def hwi_get_config(specter):
config = {
'whitelisted_domains': 'http://127.0.0.1:25441/'
}

# if config.json file exists - load from it
# if hwi_bridge_config.json file exists - load from it
if os.path.isfile(os.path.join(specter.data_folder, "hwi_bridge_config.json")):
with open(os.path.join(specter.data_folder, "hwi_bridge_config.json"), "r") as f:
file_config = json.loads(f.read())
Expand Down Expand Up @@ -273,3 +297,17 @@ def sort_descriptor(cli, descriptor, index=None, change=False):
desc = f"{p}({desc})"

return AddChecksum(desc)

def hash_password(password):
"""Hash a password for storing."""
salt = binascii.b2a_base64(hashlib.sha256(os.urandom(60)).digest()).strip()
pwdhash = binascii.b2a_base64(hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 10000)).strip().decode()
return { 'salt': salt.decode(), 'pwdhash': pwdhash }

def verify_password(stored_password, provided_password):
"""Verify a stored password against one provided by user"""
pwdhash = hashlib.pbkdf2_hmac('sha256',
provided_password.encode('utf-8'),
stored_password['salt'].encode(),
10000)
return pwdhash == binascii.a2b_base64(stored_password['pwdhash'])
6 changes: 3 additions & 3 deletions src/cryptoadvance/specter/hwi_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def api():
"error": { "code": -32700, "message": "Parse error" },
"id": None
}), 500
if ('forwarded_request' not in data or not data['forwarded_request']) and (app.specter.config['hwi_bridge_url'].startswith('http://') or app.specter.config['hwi_bridge_url'].startswith('https://')):
if ('forwarded_request' not in data or not data['forwarded_request']) and (app.specter.hwi_bridge_url.startswith('http://') or app.specter.hwi_bridge_url.startswith('https://')):
if ('HTTP_ORIGIN' not in request.environ):
return jsonify({
"jsonrpc": "2.0",
Expand All @@ -46,11 +46,11 @@ def api():
data['forwarded_request'] = True
requests_session = requests.Session()
requests_session.headers.update({'origin': request.environ['HTTP_ORIGIN']})
if '.onion/' in app.specter.config['hwi_bridge_url']:
if '.onion/' in app.specter.hwi_bridge_url:
requests_session.proxies = {}
requests_session.proxies['http'] = 'socks5h://localhost:9050'
requests_session.proxies['https'] = 'socks5h://localhost:9050'
forwarded_request = requests_session.post(app.specter.config['hwi_bridge_url'], data=json.dumps(data))
forwarded_request = requests_session.post(app.specter.hwi_bridge_url, data=json.dumps(data))
response = json.loads(forwarded_request.content)
return jsonify(response)

Expand Down
Loading