[enh] Adding optionnal recovery password + delete interface using password (#4)

* Prototype of optionnal recovery password + delete interface using password
* Use crypto/hash computation built in browser instead of code found on the interwebz..
* Adding some minimal CSS to the delete interface
* async/await in delete interface to avoid a whole indent level
This commit is contained in:
Alexandre Aubin 2017-07-24 01:55:54 +02:00 committed by GitHub
parent 425ed1c6f2
commit 255e539322
3 changed files with 192 additions and 2 deletions

View file

@ -6,3 +6,4 @@ gem 'json'
gem 'data_mapper' gem 'data_mapper'
gem 'dm-postgres-adapter' gem 'dm-postgres-adapter'
gem 'pg' gem 'pg'
gem 'bcrypt'

158
delete.html Normal file
View file

@ -0,0 +1,158 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<style>
/* Adapted from https://github.com/minimalcss/form/tree/master/demo */
input {
display: block;
margin: 0;
padding: 0;
width: 100%;
outline: 0;
border: 0;
border-radius: 0;
/*color: inherit;*/
font: inherit;
line-height: normal;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.button {
text-align:center;
color: #ffffff;
background-color: #4c9ed9;
padding-top: 0.5em;
padding-bottom:0.5em;
}
.label {
display: block;
margin-bottom: 0.25em;
}
.input {
padding: 10px;
border-width: 1px;
border-style: solid;
border-color: lightgray;
background-color: white;
}
.input:focus
{
border-color: gray;
}
.input::-webkit-input-placeholder
{
color: gray;
}
.input::-moz-placeholder
{
color: gray;
}
.input:-ms-input-placeholder
{
color: gray;
}
.input::placeholder
{
color: gray;
}
*, *:before, *:after {
box-sizing: border-box;
}
body {
margin: 2em;
font-family: sans-serif;
}
a {
color: black;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
form {
max-width: 500px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
}
.input {
margin-bottom: 1.5em;
}
</style>
<script>
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* START SHA256 CODE - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
// From https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
async function sha256(message) {
const msgBuffer = new TextEncoder('utf-8').encode(message); // encode as UTF-8
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert ArrayBuffer to Array
const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); // convert bytes to hex string
return hashHex;
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* END SHA256 CODE - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
async function sendDeleteRequest()
{
// Compute 'true' password
var domain = document.getElementById("domain").value;
var user_password = document.getElementById("password").value;
var true_password = await sha256(domain+":"+user_password);
// Prepare request
var url = "./domains/"+domain
var params = "recovery_password="+true_password;
var xhttp = new XMLHttpRequest();
// Prepare handler
xhttp.onreadystatechange = function()
{
if (xhttp.readyState == 4)
{
if (xhttp.status == 200)
{
document.getElementById("debug").innerHTML = xhttp.responseText;
}
else
{
document.getElementById("debug").innerHTML = "Error ? " + xhttp.responseText;
}
}
else
{
document.getElementById("debug").innerHTML = "Sending request...";
}
};
// Actually send the request
xhttp.open("DELETE", url, true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send(params);
}
</script>
</head>
<body>
<form>
<label class="label" for="domain">Domain to delete:</label>
<input class="input" type="text" id="domain">
<label class="label" for="password">Password:</label>
<input class="input" type="password" id="password">
<input type="button" class="button" value="Submit" onclick="sendDeleteRequest();">
<span id="debug"></span>
</form>
</body>
</html>

View file

@ -5,6 +5,7 @@ require 'sinatra'
require 'data_mapper' require 'data_mapper'
require 'json' require 'json'
require 'base64' require 'base64'
require 'bcrypt'
###################### ######################
###  Configuration ### ###  Configuration ###
@ -22,12 +23,14 @@ ALLOWED_IP = ["127.0.0.1"]
# Dynette Entry class # Dynette Entry class
class Entry class Entry
include DataMapper::Resource include DataMapper::Resource
include BCrypt
property :id, Serial property :id, Serial
property :public_key, String property :public_key, String
property :subdomain, String property :subdomain, String
property :current_ip, String property :current_ip, String
property :created_at, DateTime property :created_at, DateTime
property :recovery_password, Text
has n, :ips has n, :ips
end end
@ -130,6 +133,14 @@ get '/' do
"Wanna play the dynette ?" "Wanna play the dynette ?"
end end
# Delete interface for user with recovery password
get '/delete' do
f = File.open("delete.html", "r")
content_type 'text/html'
f.read
end
# Get availables DynDNS domains # Get availables DynDNS domains
get '/domains' do get '/domains' do
DOMAINS.to_json DOMAINS.to_json
@ -158,9 +169,17 @@ post '/key/:public_key' do
halt 409, { :error => "Key already exists for domain #{entry.subdomain}" }.to_json halt 409, { :error => "Key already exists for domain #{entry.subdomain}" }.to_json
end end
# If user provided a recovery password, hash and salt it before storing it
if params.has_key?("recovery_password")
recovery_password = BCrypt::Password.create(params[:recovery_password])
else
recovery_password = ""
end
# Process # Process
entry = Entry.new(:public_key => params[:public_key], :subdomain => params[:subdomain], :current_ip => request.ip, :created_at => Time.now) entry = Entry.new(:public_key => params[:public_key], :subdomain => params[:subdomain], :current_ip => request.ip, :created_at => Time.now, :recovery_password => recovery_password)
entry.ips << Ip.create(:ip_addr => request.ip) entry.ips << Ip.create(:ip_addr => request.ip)
if entry.save if entry.save
halt 201, { :public_key => entry.public_key, :subdomain => entry.subdomain, :current_ip => entry.current_ip }.to_json halt 201, { :public_key => entry.public_key, :subdomain => entry.subdomain, :current_ip => entry.current_ip }.to_json
else else
@ -201,10 +220,21 @@ end
# Delete a sub-domain # Delete a sub-domain
delete '/domains/:subdomain' do delete '/domains/:subdomain' do
unless ALLOWED_IP.include? request.ip unless (ALLOWED_IP.include? request.ip) || (params.has_key?("recovery_password"))
halt 403, { :error => "Access denied"}.to_json halt 403, { :error => "Access denied"}.to_json
end end
if entry = Entry.first(:subdomain => params[:subdomain]) if entry = Entry.first(:subdomain => params[:subdomain])
# For non-admin
unless (ALLOWED_IP.include? request.ip)
# If no recovery password was provided when registering domain,
# or if wrong password is provided, deny access
if (entry.recovery_password == "") || (BCrypt::Password.new(entry.recovery_password) != params[:recovery_password])
halt 403, { :error => "Access denied" }.to_json
end
end
Ip.first(:entry_id => entry.id).destroy Ip.first(:entry_id => entry.id).destroy
if entry.destroy if entry.destroy
halt 200, "OK".to_json halt 200, "OK".to_json
@ -212,6 +242,7 @@ delete '/domains/:subdomain' do
halt 412, { :error => "A problem occured during DNS deletion" }.to_json halt 412, { :error => "A problem occured during DNS deletion" }.to_json
end end
end end
halt 404
end end
# Get all registered sub-domains # Get all registered sub-domains