EIPs/EIPS/draft-dapp-html-authorizati...

449 lines
39 KiB
Markdown
Raw Normal View History

2016-05-07 05:30:33 +00:00
<pre>
EIP: draft
2016-05-07 07:00:34 +00:00
Title: safe "eth_sendTransaction" authorization via html popup
2016-05-07 05:30:33 +00:00
Author: Ronan Sandford <wighawag@gmail.com>
Created: 2016-06-05
Status: Draft
Type: Standard
</pre>
Abstract
========
2016-05-08 17:21:04 +00:00
This draft EIP describes the details of an authorization method provided by rpc enabled ethereum nodes allowing regular websites to send transactions (via ```eth_sendTransaction```) without the need to enable CORS for the website's domain. This is done by asking the user permission via an html popup served by the node itself. This allow users to to safely unlock their account while interacting with web based dapps running in their everyday web browser. The html page also allow the user to enter their password when the account is unlocked and the node allowed "personal" api via rpc.
2016-05-07 05:30:33 +00:00
Motivation
==========
2016-05-07 07:00:34 +00:00
Currently, if a user navigate to a dapp running on a website using her/his everyday browser, the dapp will have by default no access to the rpc node for security reason. The user will have to enable CORS for the website's domain in order for the dapp to work. Unfortunately if the user do so, the dapp will be able to send transaction from any unlocked account without the need for any user consent. In other word not only the user need to change its node default setting but the user is also forced to trust the dapp in order to use it. This is of course not acceptable and force existing dapps to rely on the use of workarround like:
- if the transaction is a plain ether transfer the user is asked to enter it in a dedicated trusted wallet like "Mist"
2016-05-07 05:30:33 +00:00
- For more complex case, the user is asked to enter the transaction manually via the node command line interface.
2016-05-08 17:26:03 +00:00
2016-05-07 07:00:34 +00:00
This proposal aims to provide a safe and user friendly alternative.
2016-05-08 17:21:04 +00:00
<img src="./draft-dapp-html-authorization/authorization.png">
2016-05-07 05:30:33 +00:00
2016-05-08 17:26:03 +00:00
<img src="./draft-dapp-html-authorization/authorization-password.png">
<img src="./draft-dapp-html-authorization/authorization-locked.png">
2016-05-07 05:30:33 +00:00
Specification
=============
2016-05-08 17:21:04 +00:00
In order for the mechanism to work, the node need to serve an html file via http at the url <node url>/authorization.html
2016-05-07 07:00:34 +00:00
This file will then be used by the dapp in 2 different modes (invisible iframe and popup window).
2016-05-08 17:21:04 +00:00
The invisible iframe will be embeded in the dapp to allow the dapp to send its read-only rpc call without having to enable CORS for the dapp's website domain. This is done by sending message to the iframe (via javascript ```window.postMessage```) which in turn execute the rpc call. This works since the iframe and the node share the same domain/port. The iframe first message is a message containing the string "ready" to let the parent know that it know accept messages.
2016-05-07 07:00:34 +00:00
In iframe node the html file's javascript code will ensure that no call requiring an unlocked key can be made. This is to prevent dapp for embedding the visible iframe and tricking the user into clicking the confirm button.
If the dapp requires to make an ```eth_sendTransaction``` call, the dapp will instead open a new window using the same url.
2016-05-08 17:21:04 +00:00
In this popup window mode, the html file's javascript code will alow ```eth_sendTransaction``` (not ```eth_sign``` as there is no way to display to the user the meaningfull content of the transaction to sign in a safe way) to be called. But instead of sending the call to the node directly, a confirmation dialog will be presented showing the sender and recipient addresses as well the amount being transfered along with the potential gas cost. Upon the user approving, the request will be sent and the result returned to the dapp. An error will be returned in case the user cancel the request. Similarly to the iframemode, the window first message is a message containing the string "ready" to let the opener know that it know accept messages.
The html page also check for the availability of the "personal" api and if so, will ask the user to unlock the account if necessary. The unlocking is temporary (3s) so the password will be asked again if a transaction is attempted before the end of this short time.
2016-05-07 05:30:33 +00:00
Rationale
=========
2016-05-07 07:00:34 +00:00
The design for that proposal was chosen for its simplicity and security. A previous idea was to use an oauth-like protocol in order for the user to accept or deny a transaction request. It would have required deeper code change in the node and some geth contributors argues that such change did not fit into geth code base as it would have required dapp aware code.
The current design, instead has a very simple implementation (static html file that can be shared across node's implementation) and its safeness is guarantess by browsers' cross domain policies.
2016-05-08 17:21:04 +00:00
The use of iframe/ window was required to have both security and user friendliness. The invisble iframe allow the dapp to execute read only calls without the need for user input and the window ensure the user approve before making a call. While we could have made it without the window mode by making the iframe confirmation use the native browser ```window.confirm``` dialog, this would have prevented the use of a more elegant confirmation popup that the current design allow. It also happen to be that the ```window.confirm``` is not safe in some browser as it give focus to the accept option and can be triggered automatically (https://bugs.chromium.org/p/chromium/issues/detail?id=260653).
2016-05-07 05:30:33 +00:00
Implementations
===============
2016-05-07 07:00:34 +00:00
In order to implement this design, the following html file need to be served at the url <node url>/authorization
That's it
```
2016-05-08 17:21:04 +00:00
<!DOCTYPE html>
<html>
<head>
<title>Ethereum Authorization</title>
</head>
<script>
//https://github.com/alexvandesande/blockies
!function(){function r(r){for(var t=0;t<l.length;t++)l[t]=0;for(var t=0;t<r.length;t++)l[t%4]=(l[t%4]<<5)-l[t%4]+r.charCodeAt(t)}function t(){var r=l[0]^l[0]<<11;return l[0]=l[1],l[1]=l[2],l[2]=l[3],l[3]=l[3]^l[3]>>19^r^r>>8,(l[3]>>>0)/(1<<31>>>0)}function e(){var r=Math.floor(360*t()),e=60*t()+40+"%",o=25*(t()+t()+t()+t())+"%",n="hsl("+r+","+e+","+o+")";return n}function o(r){for(var e=r,o=r,n=Math.ceil(e/2),a=e-n,l=[],c=0;o>c;c++){for(var f=[],h=0;n>h;h++)f[h]=Math.floor(2.3*t());var i=f.slice(0,a);i.reverse(),f=f.concat(i);for(var v=0;v<f.length;v++)l.push(f[v])}return l}function n(r,t,e,o,n){var a=document.createElement("canvas"),l=Math.sqrt(r.length);a.width=a.height=l*e;var c=a.getContext("2d");c.fillStyle=o,c.fillRect(0,0,a.width,a.height),c.fillStyle=t;for(var f=0;f<r.length;f++){var h=Math.floor(f/l),i=f%l;c.fillStyle=1==r[f]?t:n,r[f]&&c.fillRect(i*e,h*e,e,e)}return a}function a(t){t=t||{};var a=t.size||8,l=t.scale||4,c=t.seed||Math.floor(Math.random()*Math.pow(10,16)).toString(16);r(c);var f=t.color||e(),h=t.bgcolor||e(),i=t.spotcolor||e(),v=o(a),u=n(v,f,l,h,i);return u}var l=new Array(4);window.blockies={create:a}}();
/* bignumber.js v2.3.0 https://github.com/MikeMcl/bignumber.js/LICENCE */
!function(e){"use strict";function n(e){function E(e,n){var t,r,i,o,u,s,f=this;if(!(f instanceof E))return j&&L(26,"constructor call without new",e),new E(e,n);if(null!=n&&H(n,2,64,M,"base")){if(n=0|n,s=e+"",10==n)return f=new E(e instanceof E?e:s),U(f,P+f.e+1,k);if((o="number"==typeof e)&&0*e!=0||!new RegExp("^-?"+(t="["+N.slice(0,n)+"]+")+"(?:\\."+t+")?$",37>n?"i":"").test(s))return h(f,s,o,n);o?(f.s=0>1/e?(s=s.slice(1),-1):1,j&&s.replace(/^0\.0*|\./,"").length>15&&L(M,v,e),o=!1):f.s=45===s.charCodeAt(0)?(s=s.slice(1),-1):1,s=D(s,10,n,f.s)}else{if(e instanceof E)return f.s=e.s,f.e=e.e,f.c=(e=e.c)?e.slice():e,void(M=0);if((o="number"==typeof e)&&0*e==0){if(f.s=0>1/e?(e=-e,-1):1,e===~~e){for(r=0,i=e;i>=10;i/=10,r++);return f.e=r,f.c=[e],void(M=0)}s=e+""}else{if(!g.test(s=e+""))return h(f,s,o);f.s=45===s.charCodeAt(0)?(s=s.slice(1),-1):1}}for((r=s.indexOf("."))>-1&&(s=s.replace(".","")),(i=s.search(/e/i))>0?(0>r&&(r=i),r+=+s.slice(i+1),s=s.substring(0,i)):0>r&&(r=s.length),i=0;48===s.charCodeAt(i);i++);for(u=s.length;48===s.charCodeAt(--u););if(s=s.slice(i,u+1))if(u=s.length,o&&j&&u>15&&(e>y||e!==d(e))&&L(M,v,f.s*e),r=r-i-1,r>z)f.c=f.e=null;else if(G>r)f.c=[f.e=0];else{if(f.e=r,f.c=[],i=(r+1)%O,0>r&&(i+=O),u>i){for(i&&f.c.push(+s.slice(0,i)),u-=O;u>i;)f.c.push(+s.slice(i,i+=O));s=s.slice(i),i=O-s.length}else i-=u;for(;i--;s+="0");f.c.push(+s)}else f.c=[f.e=0];M=0}function D(e,n,t,i){var o,u,f,c,a,h,g,p=e.indexOf("."),d=P,m=k;for(37>t&&(e=e.toLowerCase()),p>=0&&(f=J,J=0,e=e.replace(".",""),g=new E(t),a=g.pow(e.length-p),J=f,g.c=s(l(r(a.c),a.e),10,n),g.e=g.c.length),h=s(e,t,n),u=f=h.length;0==h[--f];h.pop());if(!h[0])return"0";if(0>p?--u:(a.c=h,a.e=u,a.s=i,a=C(a,g,d,m,n),h=a.c,c=a.r,u=a.e),o=u+d+1,p=h[o],f=n/2,c=c||0>o||null!=h[o+1],c=4>m?(null!=p||c)&&(0==m||m==(a.s<0?3:2)):p>f||p==f&&(4==m||c||6==m&&1&h[o-1]||m==(a.s<0?8:7)),1>o||!h[0])e=c?l("1",-d):"0";else{if(h.length=o,c)for(--n;++h[--o]>n;)h[o]=0,o||(++u,h.unshift(1));for(f=h.length;!h[--f];);for(p=0,e="";f>=p;e+=N.charAt(h[p++]));e=l(e,u)}return e}function F(e,n,t,i){var o,u,s,c,a;if(t=null!=t&&H(t,0,8,i,w)?0|t:k,!e.c)return e.toString();if(o=e.c[0],s=e.e,null==n)a=r(e.c),a=19==i||24==i&&B>=s?f(a,s):l(a,s);else if(e=U(new E(e),n,t),u=e.e,a=r(e.c),c=a.length,19==i||24==i&&(u>=n||B>=u)){for(;n>c;a+="0",c++);a=f(a,u)}else if(n-=s,a=l(a,u),u+1>c){if(--n>0)for(a+=".";n--;a+="0");}else if(n+=u-c,n>0)for(u+1==c&&(a+=".");n--;a+="0");return e.s<0&&o?"-"+a:a}function _(e,n){var t,r,i=0;for(u(e[0])&&(e=e[0]),t=new E(e[0]);++i<e.length;){if(r=new E(e[i]),!r.s){t=r;break}n.call(t,r)&&(t=r)}return t}function x(e,n,t,r,i){return(n>e||e>t||e!=c(e))&&L(r,(i||"decimal places")+(n>e||e>t?" out of range":" not an integer"),e),!0}function I(e,n,t){for(var r=1,i=n.length;!n[--i];n.pop());for(i=n[0];i>=10;i/=10,r++);return(t=r+t*O-1)>z?e.c=e.e=null:G>t?e.c=[e.e=0]:(e.e=t,e.c=n),e}function L(e,n,t){var r=new Error(["new BigNumber","cmp","config","div","divToInt","eq","gt","gte","lt","lte","minus","mod","plus","precision","random","round","shift","times","toDigits","toExponential","toFixed","toFormat","toFraction","pow","toPrecision","toString","BigNumber"][e]+"() "+n+": "+t);throw r.name="BigNumber Error",M=0,r}function U(e,n,t,r){var i,o,u,s,f,l,c,a=e.c,h=S;if(a){e:{for(i=1,s=a[0];s>=10;s/=10,i++);if(o=n-i,0>o)o+=O,u=n,f=a[l=0],c=f/h[i-u-1]%10|0;else if(l=p((o+1)/O),l>=a.length){if(!r)break e;for(;a.length<=l;a.push(0));f=c=0,i=1,o%=O,u=o-O+1}else{for(f=s=a[l],i=1;s>=10;s/=10,i++);o%=O,u=o-O+i,c=0>u?0:f/h[i-u-1]%10|0}if(r=r||0>n||null!=a[l+1]||(0>u?f:f%h[i-u-1]),r=4>t?(c||r)&&(0==t||t==(e.s<0?3:2)):c>5||5==c&&(4==t||r||6==t&&(o>0?u>0?f/h[i-u]:0:a[l-1])%10&1||t==(e.s<0?8:7)),1>n||!a[0])return a.length=0,r?(n-=e.e+1,a[0]=h[(O-n%O)%O],e.e=-n||0):a[0]=e.e=0,e;if(0==o?(a.length=l,s=1,l--):(a.length=l+1,s=h[O-o],a[l]=u>0?d(f/h[i-u]%h[u])*s:0),r)for(;;){if(0==l){for(o=1,u=a[0];u>=10;u/=10,o++);for(u=a[0]+=s,s=1;u>=10;u/=10,s++);o!=s&&(e.e++,a[0]==b&&(a[0]=1));break}if(a[l]+=s,a[l]!=b)break;a[l--]=0,s=1}for(o=a.length;0===a[--o];a.pop());}e.e>z?e.c=e.e=null:e.e<G&&(e.c=[e.e=0])}return e}
</script>
<style>
body{
font-family: 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', 'Helvetica', 'Arial', 'Lucida Grande', sans-serif;
background: #E2E2E2;
}
*, *:after, *:before {
box-sizing: border-box;
margin: 0;
padding: 0;
}
#pleasewait{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#infomessage {
text-align: center;
font-size: 1rem;
margin: 0 2rem 4.5rem;
}
.wrapper{
background: #E2E2E2;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
display:none;
text-align: center;
}
.title {
text-align: center;
font-size: 1.2rem;
margin: 1rem 0rem;
}
.message {
text-align: center;
font-size: 1rem;
/*margin: 0 2rem 4.5rem;*/
}
#passwordField {
text-align: center;
font-size: 1rem;
margin: 1rem 0rem;
/*margin: 0 2rem 4.5rem;*/
}
.wrapper button {
background: transparent;
border: none;
color: #1678E5;
height: 3rem;
font-size: 1rem;
width: 50%;
position: absolute;
bottom: 0;
cursor: pointer;
}
#cancel-button {
border-top: 1px solid #B4B4B4;
border-right: 1px solid #B4B4B4;
left: 0;
border-radius: 0 0 0 10px;
}
#confirm-button {
border-top: 1px solid #B4B4B4;
right: 0;
border-radius: 0 0 10px 0;
}
.wrapper button:focus,
.wrapper button:hover {
font-weight: bold;
background: #EFEFEF;
}
.wrapper button:active {
background: #D6D6D6;
}
.button {
margin: 1rem 0rem;
display: inline-block;
padding: 9px 15px;
background-color: #3898EC;
color: white;
border: 0;
line-height: inherit;
text-decoration: none;
cursor: pointer;
border-radius: 0;
}
input.button {
-webkit-appearance: button;
}
</style>
<body>
<div id="pleasewait">
<br/>
<p id="infomessage">Please wait...</p>
</div>
<div id="form" class="wrapper">
<br/>
<p id="message" class="message"></p>
<p id="passwordField"><label>Password Required:</label><input id="password" type="password" /></p>
<button id="cancel-button" autofocus>Cancel</button>
<button id="confirm-button" >Confirm</button>
</div>
<div id="modal-dialog" class="wrapper">
<h3 id="modal-dialog-title" class="title">Title</h3>
<p id="modal-dialog-message" class="message">Message</p>
<span id="modal-dialog-button" class="button">Ok</span>
</div>
<script>
var pleaseWait = document.getElementById("pleasewait");
var form = document.getElementById("form");
var cancelButton = document.getElementById("cancel-button");
var confirmButton = document.getElementById("confirm-button");
var message = document.getElementById("message");
var password = document.getElementById("password");
var passwordField = document.getElementById("passwordField");
var modalDialog = document.getElementById("modal-dialog");
var modalDialogButton = document.getElementById("modal-dialog-button");
var modalDialogTitle = document.getElementById("modal-dialog-title");
var modalDialogMessage = document.getElementById("modal-dialog-message");
var firstUrl = null;
var inIframe = true;
var source = null;
if(window.opener){
inIframe = false;
source = window.opener;
}else if(window.parent != window){
source = window.parent;
}else{
console.error("no opener nor parent");
}
function showWaiting(){
pleaseWait.style.display = "block";
form.style.display = "none";
}
function hideWaiting(){
pleaseWait.style.display = "none";
form.style.display = "block";
}
function showMessage(title, message, callback, buttonText){
modalDialog.style.display = "block";
modalDialogTitle.innerHTML = title;
modalDialogMessage.innerHTML = "";
if((typeof message) == "string"){
modalDialogMessage.innerHTML += message;
}else{
modalDialogMessage.appendChild(message);
}
modalDialogMessage.appendChild(document.createElement('br'));
if(!buttonText){
buttonText = "Ok";
}
modalDialogButton.innerHTML = buttonText;
modalDialogButton.onclick = function(){
modalDialogButton.onclick = null;
modalDialog.style.display = "none";
if(callback){
callback();
}
}
}
function sendAsync(url,payload, callback) {
//var url = window.location.protocol + '//' + window.location.host; // this would force the use of the iframe url
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type','application/json');
request.onreadystatechange = function() {
if (request.readyState === 4) {
var result = request.responseText;
var error = null;
try {
result = JSON.parse(result);
} catch(e) {
var message = !!result && !!result.error && !!result.error.message ? result.error.message : 'Invalid JSON RPC response: ' + JSON.stringify(result);
error = {message:message};
}
callback(error, result);
}
};
try {
request.send(JSON.stringify(payload));
} catch(e) {
callback({message:'CONNECTION ERROR: Couldn\'t connect to node '+ url +'.'});
}
}
function addBlocky(message, address){
var icon = blockies.create({
seed: address,
size: 8,
scale: 6
});
//icon.style="vertical-align:middle";
message.appendChild(icon); //
}
function askAuthorization(transactionInfo, data, requireUnlock, sourceWindow){
var value = transactionInfo["value"] ? transactionInfo.value : "0";
var gasProvided = transactionInfo.gas;
var gasPriceProvided = transactionInfo.gasPrice;
var gasPrice = new BigNumber(gasPriceProvided,16);
var gas = new BigNumber(gasProvided,16);
var weiValue = new BigNumber(value,16);
var gasWeiValue = gas.times(gasPrice);
var etherValue = weiValue.dividedBy(new BigNumber("1000000000000000000"));
var gasEtherValue = gasWeiValue.dividedBy(new BigNumber("1000000000000000000"));
hideWaiting();
message.innerHTML = "";
addBlocky(message,transactionInfo.from);
var span = document.createElement('span');
span.style="font-size:3em;";
span.innerHTML = "&nbsp;&nbsp;&nbsp;" + "&#x2192;" + "&nbsp;&nbsp;&nbsp;";
//span.innerHTML = "&#10145;";
message.appendChild(span);
addBlocky(message,transactionInfo.to);
message.appendChild(document.createElement('br'));
var textSpan = document.createElement("span");
message.appendChild(textSpan);
textSpan.innerHTML = etherValue.toFormat() + " ether <br/> + gas cost (" + gasEtherValue.toFormat() + " ether )"
if(requireUnlock){
passwordField.style.display = "block"; //inline ?
}else{
passwordField.style.display = "none";
}
cancelButton.onclick = function(){
sourceWindow.postMessage({id:data.id,result:null,error:{message:"Not Authorized"}},sourceWindow.location.href);
window.close();
}
confirmButton.onclick = function(){
if(requireUnlock){
if(password.value == ""){
password.style.border = "2px solid red";
return;
}
password.style.border = "none";
var params = [transactionInfo.from,password.value,3];
showWaiting();
sendAsync(data.url,{id:999992,method:"personal_unlockAccount",params:params},function(error,result){
if(error || result.error){
showMessage("Error unlocking account", "Please retry.", hideWaiting);
}else{
sendAsync(data.url,data.payload,function(error,result){ //data.url to allow use of different but trusted domain
sourceWindow.postMessage({id:data.id,result:result,error:error},sourceWindow.location.href);
window.close();
});
showWaiting();
}
});
}else{
sendAsync(data.url,data.payload,function(error,result){ //data.url to allow use of different but trusted domain
if(result && result.error){
processMessage(data,sourceWindow);
}else{
sourceWindow.postMessage({id:data.id,result:result,error:error},sourceWindow.location.href);
window.close();
}
});
showWaiting();
}
}
}
function needToAndCanUnlockAccount(address,url,callback){
sendAsync(url,{id:9999990,method:"eth_sign",params:[address,"0xc6888fa8d57087278718986382264244252f8d57087278718986382264244252f"]},function(error,result){
if(error || result.error){
sendAsync(url,{id:9999991,method:"personal_listAccounts",params:[]},function(error,result){
if(error || result.error){
callback(true,false);
}else{
callback(true,true);
}
});
}else{
callback(false);
}
});
}
function receiveMessage(event){
if(firstUrl){
if(firstUrl != event.origin){
return;
}
}else{
firstUrl = event.origin;
}
var data = event.data;
processMessage(data,event.source);
}
function processMessage(data, sourceWindow){
if(data.payload.method == "eth_sendTransaction" || data.payload.method == "eth_sign"){
if(inIframe){
sourceWindow.postMessage({id:data.id,result:null,error:{message:"Cannot make call that require an unlokced key (" + data.payload.method + ") via iframe"}},sourceWindow.location.href);
}else if(data.payload.method == "eth_sign"){
sourceWindow.postMessage({id:data.id,result:null,error:{message:"cannot sign transaction (" + data.payload.method + ") via html"}},sourceWindow.location.href);
}else{
var transactionInfo = null;
if(data.payload.params.length > 0){
if(data.payload.params[0]["gas"] && data.payload.params[0]["gasPrice"] && data.payload.params[0]["to"] && data.payload.params[0]["from"]){
transactionInfo = data.payload.params[0];
}
}
if(transactionInfo != null){
needToAndCanUnlockAccount(transactionInfo.from,data.url,function(requireUnlock,canUnlock){
if(requireUnlock && canUnlock){
askAuthorization(transactionInfo,data,true, sourceWindow);
}else if(!requireUnlock){
askAuthorization(transactionInfo,data,false,sourceWindow);
}else if(requireUnlock && !canUnlock){
var messageHtml = document.createElement('span');
addBlocky(messageHtml,transactionInfo.from);
messageHtml.appendChild(document.createElement('br'));
var span = document.createElement('span');
span.innerHTML = "You need to unlock your account first : <br/>" + transactionInfo.from;
messageHtml.appendChild(span);
showMessage("Account Locked",messageHtml,function(){
processMessage(data,sourceWindow);
}, "Done");
}
});
}else{
sourceWindow.postMessage({id:data.id,result:null,error:{message:"Need to specify from , to, gas and gasPrice"}},sourceWindow.location.href);
window.close();
}
}
}else{
sendAsync(data.url,data.payload,function(error,result){ //data.url to allow use of different but trusted domain
sourceWindow.postMessage({id:data.id,result:result,error:error},sourceWindow.location.href);
});
}
}
window.addEventListener("message", receiveMessage);
if(source){
source.postMessage("ready",source.location.href);
}
</script>
</body>
</html>
2016-05-07 07:00:34 +00:00
```