Add seed restore functionality (#2)

minor css adjustment

simplify wallet init and enter teh matrix

add another check on seed inputs

add stronger language around wallet deletion and seed restores

add command to manually reset wallet data for a user

update connect payload and adjust minor template/js

remove debug statements

refactor loading javascript, html, and status payload

remove unused ref to .map

tighten up logic and html template

save template updates

Merge branch 'master' of git.wownero.com:lza_menace/wowstash into seed-restores

modify workflow to allow seed restoral

Co-authored-by: lza_menace <lza_menace@protonmail.com>
Reviewed-on: #2
event-logs
lza_menace 3 år sedan
förälder 3a9c13919d
incheckning e8e97c9f1c

@ -3,5 +3,5 @@
source .venv/bin/activate
export FLASK_APP=wowstash/app.py
export FLASK_SECRETS=config.py
export FLASK_DEBUG=1
flask $1
export FLASK_DEBUG=0
flask $@

@ -35,7 +35,7 @@ def register():
# Capture event, login user and redirect to wallet page
send_es({'type': 'register', 'user': user.email})
login_user(user)
return redirect(url_for('wallet.dashboard'))
return redirect(url_for('wallet.setup'))
return render_template("auth/register.html", form=form)
@ -91,7 +91,7 @@ def delete():
send_es({'type': 'delete_wallet', 'user': current_user.email})
current_user.clear_wallet_data(reset_password=True, reset_wallet=True)
flash('Successfully deleted wallet data')
return redirect(url_for('meta.index'))
return redirect(url_for('wallet.setup'))
else:
flash('Please confirm deletion of the account')
return redirect(url_for('wallet.dashboard'))

@ -13,18 +13,38 @@ from wowstash.library.docker import docker
from wowstash.library.elasticsearch import send_es
from wowstash.library.jsonrpc import Wallet, to_atomic
from wowstash.library.cache import cache
from wowstash.forms import Send, Delete
from wowstash.forms import Send, Delete, Restore
from wowstash.factory import db
from wowstash.models import User
from wowstash import config
@wallet_bp.route('/wallet/setup', methods=['GET', 'POST'])
@login_required
def setup():
if current_user.wallet_created:
return redirect(url_for('wallet.dashboard'))
else:
restore_form = Restore()
if restore_form.validate_on_submit():
c = docker.create_wallet(current_user.id, restore_form.seed.data)
cache.store_data(f'init_wallet_{current_user.id}', 30, c)
current_user.wallet_created = True
db.session.commit()
return redirect(url_for('wallet.loading'))
else:
return render_template(
'wallet/setup.html',
restore_form=restore_form
)
@wallet_bp.route('/wallet/loading')
@login_required
def loading():
if current_user.wallet_connected and current_user.wallet_created:
sleep(1)
return redirect(url_for('wallet.dashboard'))
if current_user.wallet_created is False:
return redirect(url_for('wallet.setup'))
return render_template('wallet/loading.html')
@wallet_bp.route('/wallet/dashboard')
@ -46,6 +66,7 @@ def dashboard():
return redirect(url_for('wallet.loading'))
if not wallet.connected:
sleep(1.5)
return redirect(url_for('wallet.loading'))
address = wallet.get_address()
@ -79,6 +100,13 @@ def dashboard():
@wallet_bp.route('/wallet/connect')
@login_required
def connect():
if current_user.wallet_created is False:
data = {
'result': 'fail',
'message': 'Wallet not yet created'
}
return jsonify(data)
if current_user.wallet_connected is False:
wallet = docker.start_wallet(current_user.id)
port = docker.get_port(wallet)
@ -87,27 +115,42 @@ def connect():
current_user.wallet_container = wallet
current_user.wallet_start = datetime.utcnow()
db.session.commit()
data = {
'result': 'success',
'message': 'Wallet has been connected'
}
else:
data = {
'result': 'fail',
'message': 'Wallet is already connected'
}
return 'ok'
return jsonify(data)
@wallet_bp.route('/wallet/create')
@login_required
def create():
if current_user.wallet_created is False:
docker.create_wallet(current_user.id)
c = docker.create_wallet(current_user.id)
cache.store_data(f'init_wallet_{current_user.id}', 30, c)
current_user.wallet_created = True
db.session.commit()
return 'ok'
return redirect(url_for('wallet.loading'))
else:
return redirect(url_for('wallet.dashboard'))
@wallet_bp.route('/wallet/status')
@login_required
def status():
user_vol = docker.get_user_volume(current_user.id)
create_container = cache.get_data(f'init_wallet_{current_user.id}')
data = {
'created': current_user.wallet_created,
'connected': current_user.wallet_connected,
'port': current_user.wallet_port,
'container': current_user.wallet_container
'container': current_user.wallet_container,
'volume': docker.volume_exists(user_vol),
'initializing': docker.container_exists(create_container)
}
return jsonify(data)

@ -1,3 +1,4 @@
import click
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
@ -68,6 +69,14 @@ def create_app():
from wowstash.library.docker import docker
docker.cleanup()
@app.cli.command('reset_wallet')
@click.argument('user_id')
def reset_wallet(user_id):
from wowstash.models import User
user = User.query.get(user_id)
user.clear_wallet_data()
print(f'Wallet data cleared for user {user.id}')
# Routes/blueprints
from wowstash.blueprints.auth import auth_bp
from wowstash.blueprints.wallet import wallet_bp

@ -1,6 +1,7 @@
from re import match as re_match
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired
from wtforms.validators import DataRequired, ValidationError
class Register(FlaskForm):
@ -20,3 +21,14 @@ class Send(FlaskForm):
class Delete(FlaskForm):
confirm = BooleanField('Confirm Account and Wallet Deletion:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
class Restore(FlaskForm):
seed = StringField('Seed Phrase', validators=[DataRequired()], render_kw={"placeholder": "25 word mnemonic seed phrase", "class": "form-control"})
risks_accepted = BooleanField('I accept the risks:', validators=[DataRequired()], render_kw={"class": "form-control-span"})
def validate_seed(self, seed):
regex = '^[\w\s]+$'
if bool(re_match(regex, self.seed.data)) is False:
raise ValidationError('Invalid seed provided; must be alphanumeric characters only')
if len(self.seed.data.split()) != 25:
raise ValidationError("Invalid seed provided; must be standard Wownero 25 word format")

@ -17,6 +17,13 @@ class Cache(object):
value=data
)
def get_data(self, item_name):
data = self.redis.get(item_name)
if data:
return data.decode()
else:
return None
def get_coin_info(self):
info = self.redis.get("coin_info")
if info:

@ -19,21 +19,34 @@ class Docker(object):
self.wallet_dir = expanduser(getattr(config, 'WALLET_DIR', '~/data/wallets'))
self.listen_port = 8888
def create_wallet(self, user_id):
def create_wallet(self, user_id, seed=None):
u = User.query.get(user_id)
volume_name = self.get_user_volume(u.id)
u.wallet_password = token_urlsafe(12)
db.session.commit()
command = f"""wownero-wallet-cli \
--generate-new-wallet /wallet/{u.id}.wallet \
--restore-height {daemon.info()['height']} \
--password {u.wallet_password} \
--mnemonic-language English \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--log-file /wallet/{u.id}-create.log
--command version
"""
if seed:
command = f"""sh -c "yes '' | wownero-wallet-cli \
--restore-deterministic-wallet \
--generate-new-wallet /wallet/{u.id}.wallet \
--restore-height 0 \
--password {u.wallet_password} \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--electrum-seed '{seed}' \
--log-file /wallet/{u.id}-init.log \
--command refresh"
"""
else:
command = f"""wownero-wallet-cli \
--generate-new-wallet /wallet/{u.id}.wallet \
--restore-height {daemon.info()['height']} \
--password {u.wallet_password} \
--mnemonic-language English \
--daemon-address {config.DAEMON_PROTO}://{config.DAEMON_HOST}:{config.DAEMON_PORT} \
--daemon-login {config.DAEMON_USER}:{config.DAEMON_PASS} \
--log-file /wallet/{u.id}-init.log \
--command version
"""
if not self.volume_exists(volume_name):
self.client.volumes.create(
name=volume_name,
@ -43,7 +56,7 @@ class Docker(object):
self.wownero_image,
command=command,
auto_remove=True,
name=f'create_wallet_{u.id}',
name=f'init_wallet_{u.id}',
remove=True,
detach=True,
volumes={
@ -53,7 +66,7 @@ class Docker(object):
}
}
)
send_es({'type': 'create_wallet', 'user': u.email})
send_es({'type': f'init_wallet', 'user': u.email})
return container.short_id
def start_wallet(self, user_id):

@ -552,3 +552,82 @@ ol li {
border-radius: 4px;
padding: 6px;
}
.teh_matrix {
margin: 2em auto;
}
.teh_matrix img.doge {
opacity: 0;
position: absolute;
top: 10px;
left: 40px;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.centered {
opacity: 0;
display: none;
color: #02ff44;
font-weight: bold;
font-size: 20px;
position: absolute;
top: 88px;
width: 400px;
text-align: center;
font-family: monospace;
word-break: break-word;
margin: 20px;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.title {
opacity: 0;
color: #02ff44;
font-weight: bold;
font-size: 18px;
position: absolute;
top: 28px;
left: 100px;
width: 300px;
font-family: monospace;
word-break: break-word;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}
.teh_matrix span.body {
position: absolute;;
opacity: 0;
color: #02ff44;
font-weight: bold;
font-size: 18px;
top: 70px;
left: 100px;
width: 300px;
font-family: monospace;
word-break: break-word;
text-shadow: black 1px 1px 0;
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-ms-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
}

Binary file not shown.

Efter

Bredd:  |  Höjd:  |  Storlek: 216 KiB

File diff suppressed because one or more lines are too long

@ -14,7 +14,11 @@
<div class="header-content mx-auto">
<h1 class="mb-5">Manage your Wownero funds securely and anonymously.</h1>
{% if current_user.is_authenticated %}
<a href="{{ url_for('wallet.dashboard') }}" class="btn btn-outline btn-xl">{% if current_user.wallet_created %}Wallet Dashboard{% else %}Create Wallet{% endif %}</a>
{% if current_user.wallet_created %}
<a href="{{ url_for('wallet.dashboard') }}" class="btn btn-outline btn-xl">Wallet Dashboard</a>
{% else %}
<a href="{{ url_for('wallet.setup') }}" class="btn btn-outline btn-xl">Setup Wallet</a>
{% endif %}
{% else %}
<a href="{{ url_for('auth.register') }}" class="btn btn-outline btn-xl">Register</a>
<a href="{{ url_for('auth.login') }}" class="btn btn-outline btn-xl">Login</a>

@ -4,6 +4,80 @@
<script src="/static/js/main.js"></script>
<script src="/static/js/noty.js"></script>
{% if request.path == '/wallet/loading' %}
<script type="text/javascript">
function check_status(){
fetch('/wallet/status')
.then((resp) => resp.json())
.then(function(data) {
// If we've created a wallet and volume, but not connected a container and are not restoring, attempt connecting
if(data['created'] && data['volume'] && data['connected'] == false && data['initializing'] == false){
fetch('/wallet/connect')
}
// If all of the above and now the wallet is connected, go to dashboard
if(data['created'] && data['volume'] && data['connected']){
window.setInterval(function(){
window.location.href = "{{ url_for('wallet.dashboard') }}"
}, 3000);
}
})
}
$(document).ready(function () {
// Check wallet status every few seconds...
window.setInterval(function(){
check_status();
}, 7000);
// ...but also check on initial page load
check_status();
// enter teh matrix
let q = document.getElementById('q');
let width = q.width;
let height = q.height;
let yPositions = Array(300).join(0).split('');
let ctx = q.getContext('2d');
let draw = function () {
ctx.fillStyle = 'rgba(0,0,0,.05)';
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = '#0F0';
ctx.font = '10pt Georgia';
yPositions.map(function (y, index) {
let text = String.fromCharCode(1e2 + Math.random() * 33);
let x = (index * 10) + 10;
q.getContext('2d').fillText(text, x, y);
if (y > 100 + Math.random() * 1e4) {
yPositions[index] = 0;
}
else {
yPositions[index] = y + 10;
}
});
};
let matrix_interval = null;
function RunMatrix() {
matrix_interval = setInterval(draw, 33);
}
RunMatrix();
$('.teh_matrix span').each(function(i, obj){
jQuery(obj).css('opacity', '1');
});
$('.teh_matrix img').each(function(i, obj){
jQuery(obj).css('opacity', '1');
});
});
</script>
{% endif %}
{% if request.path == '/wallet/dashboard' %}
<script type="text/javascript" src="/static/js/zxing.js"></script>
<script type="text/javascript">

@ -154,6 +154,7 @@
<div class="section-heading text-center">
<h2>Delete Account</h2>
<p>You can and should delete your wallet from the server. Please ensure you have copied the mnemonic seed from the secrets above if there are still funds associated with the keys.</p>
<p>I highly recommend making a new wallet on your own and transferring funds there to ensure only you have full ownership and visibility into the private keys / seed. Not your keys, not your crypto!</p>
<form method="POST" action="{{ url_for('auth.delete') }}" class="send-form">
{{ delete_form.csrf_token }}
{% for f in delete_form %}
@ -168,7 +169,7 @@
{% for field, errors in delete_form.errors.items() %}
<li>{{ send_form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %}
</ul>
</ul>
<input type="submit" value="Delete" class="btn btn-link btn-outline btn-xl">
</form>
</div>

@ -5,67 +5,32 @@
<body id="page-top">
{% include 'navbar.html' %}
<section class="section2">
<section class="section1">
<div class="container">
<div class="section-heading text-center">
{% if current_user.wallet_created == False %}
<h2>Your wallet is being created</h2>
{% else %}
<h2>Your wallet is connecting</h2>
{% endif %}
<p>Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below</p>
<img src="/static/img/loading-cat.gif" width=300>
<p>Go smoke a fatty. This page should auto-refresh when it's ready...if not, click the button below. <br /><br />If you are restoring from a seed, please allow several minutes for the process to complete.</p>
<div class="teh_matrix" style="position: relative;border-radius:4px;width:450px;margin-bottom:32px;border:1px solid green;height:250px;overflow:hidden;background-color:black;">
<canvas id="q" width="450px" height="250px"></canvas>
<span class="centered"></span>
<img class="doge" width=80px src="/static/img/loading-doge.png"/>
<span class="title">Very Secure Login™</span>
<span class="body">
☑ many encryptions<br>
☑ very password<br>
☑ NASA certified<br>
☑ such login<br>
</span>
</div>
<span class="dashboard-buttons">
<div class="col-sm-12 dashboard-button">
<a class="btn btn-lg btn-link btn-outline btn-xl" href="{{ url_for('wallet.dashboard') }}">Check Again</a>
<a class="btn-link" href="{{ url_for('wallet.dashboard') }}">Check Again</a>
</div>
</span>
</div>
</div>
</section>
<script>
function check_wallet_status(attrib) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4){
let res = JSON.parse(xhr.responseText);
if (res[attrib] == true) {
window.location.href = "{{ url_for('wallet.dashboard') }}"
}
}
};
xhr.open('GET', '{{ url_for("wallet.status") }}');
xhr.send();
}
{% if current_user.wallet_connected == False and current_user.wallet_created == True %}
document.addEventListener("DOMContentLoaded", function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("wallet.connect") }}');
xhr.send();
});
{% endif %}
{% if current_user.wallet_connected == False and current_user.wallet_created == False %}
document.addEventListener("DOMContentLoaded", function(){
var xhr = new XMLHttpRequest();
xhr.open('GET', '{{ url_for("wallet.create") }}');
xhr.send();
});
{% endif %}
window.setInterval(function(){
{% if current_user.wallet_connected == False and current_user.wallet_created == True %}
check_wallet_status('connected');
{% else %}
check_wallet_status('created');
{% endif %}
}, 6000);
</script>
{% include 'footer.html' %}
{% include 'scripts.html' %}

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
{% include 'head.html' %}
<body id="page-top">
{% include 'navbar.html' %}
<section class="section2">
<div class="container">
<div class="section-heading text-center">
<h2>Setup Wallet</h2>
<p>Alrighty there hoss, pick an option below...</p>
<hr><br /><br />
<a class="btn btn-lg btn-link btn-outline btn-xl" href="{{ url_for('wallet.create') }}">Create new wallet</a>
<hr><br /><br />
<form method="POST" action="{{ url_for('wallet.setup') }}" class="send-form">
<p><strong>! WARNING !</strong><br /> If you input a mnemonic seed here I could theoretically steal your funds, even without a wallet on my server; so could a hacker if they compromised my server.</p>
<p>You <strong>can</strong> and <strong>should</strong> use a <a href="https://wownero.org/#wallets" target="_blank">wallet</a> you can run locally to ensure your funds are safe, especially if there is a lot there. Proceed at your own risk.</p>
{{ restore_form.csrf_token }}
{% for f in restore_form %}
{% if f.name != 'csrf_token' %}
<div class="form-group">
{{ f.label }}
{{ f }}
</div>
{% endif %}
{% endfor %}
<ul>
{% for field, errors in restore_form.errors.items() %}
<li>{{ restore_form[field].label }}: {{ ', '.join(errors) }}</li>
{% endfor %}
</ul>
<input type="submit" value="Restore From Seed" class="btn btn-link btn-outline btn-xl">
</form>
</div>
</div>
</section>
{% include 'footer.html' %}
{% include 'scripts.html' %}
</body>
</html>