Fix Conflicts:

js/controllers/signin.js
This commit is contained in:
Gustavo Cortez 2014-05-06 18:12:24 -03:00
commit 88a7fca8d8
12 changed files with 293 additions and 113 deletions

View File

@ -36,19 +36,19 @@ About Copay
General
-------
*Copay* implements a multisig wallet using p2sh addresses. It support multiple wallet configurations, like 3-of-5
(3 required signatures from 5 participant peers) or 2-of-3. To generate addresses to receive coins,
*Copay* needs the public keys of all the participat peers in the wallet. Those public keys, among the
*Copay* implements a multisig wallet using p2sh addresses. It supports multiple wallet configurations, like
3-of-5 (3 required signatures from 5 participant peers) or 2-of-3. To generate addresses to receive coins,
*Copay* needs the public keys of all the participant peers in the wallet. Those public keys, among the
wallet configuration, are combined to generate a single address to receive a payment.
To unlock the payment, and spend the wallet's funds, the needed signatures need to be collected an put togheter
in the transaction. Each peer manage her own private key, and that key is never transmited to other
peers. Once a transaction proposal is created, the proposal is distributed among the peers and each peer
can sign the transaction locally. Once the transaction is complete, the last signing peer will broadcast the
To unlock the payment, and spend the wallet's funds, the needed signatures need to be collected and put
togheter in the transaction. Each peer manages her own private key, and that key is never transmited to
other peers. Once a transaction proposal is created, the proposal is distributed among the peers and each peer
can sign the transaction locally. Once the signing is complete, the last signing peer will broadcast the
transaction to the bitcoin network, using a public API for that (Insight API by default in *Copay*)..
*Copay* also implements BIP32 to generate new addresses for the peers. This mean that the actual piece of
information shared between the peers is an extended public key, from which is possible to derive more
*Copay* also implements BIP32 to generate new addresses for the peers. This means that the actual piece of
information shared between the peers is an extended public key, from which it is possible to derive more
public keys so the wallet can use them. Each peer holds for himself his extended private key, to be able
to sign the incoming transaction proposals.
@ -57,41 +57,40 @@ Serverless web
*Copay* software does not need an application server to run. All the software is implemented in client-side
Javascript. For persistent storage, the client browser's *localStorage* is used. This information is
stored encryped using the peer's password. Also it is possible (and recommended) to backup that information
with using one of the options provided by *Copay*, like file downloading. Without a proper backup, all
with using one of the options provided by *Copay*, like file downloading. Without a proper backup, all
wallets funds can be lost if the browser's localStorage is deleted, or the browser installation deleted.
Peer communications
-------------------
*Copay* use peer-to-peer (p2p) networking to comunicate the parties. Parties exchange transaction
proposals, public keys, nicknames and some wallet options. As mentioned above, private keys are *no*
*Copay* use peer-to-peer (p2p) networking for comunication between the parties. Parties exchange transaction
proposals, public keys, nicknames and some wallet options. As mentioned above, private keys are *not*
sent to the network.
webRTC is the used protocol. A p2p facilitator server is needed to allow the peers to find each other.
*Copay* uses the open-sourced *peerjs*
server implementation. Wallet participants can use a public peerjs server or install their own. Once the peers
find each other, a true p2p connection is established and there is no flow of information to the
server, only between the peers.
webRTC is the used protocol for p2p communication. A p2p facilitator server is needed to allow the peers
to find each other. *Copay* uses the open-sourced *peerjs* server implementation. Wallet participants
can use a public peerjs server or install their own. Once the peers find each other, a true p2p
connection is established and there is no flow of information to the server, only between the peers.
webRTC uses DTLS to secure communications between the peers, and each peer use a self-signed
webRTC uses DTLS to secure communications between the peers, and each peer uses a self-signed
certificate.
Security model
--------------
On top of webRTC, *Copay* peers authenticate as part of the "wallet ring"(WR) by 2 factors: An identity
On top of webRTC, *Copay* peers authenticate as part of the "wallet ring"(WR) by 2 factors: An identity
key and a network key.
The *identity key* is a ECDSA public key derived from their extended public
key using a specific BIP32 branch. This special public key is never used for Bitcoin address creation, and
should only be know by members of the WR.
In *Copay* this special public key is named *copayerId*. To register into the peerjs server, while not
reveling its copayerId to an entity outside the WR, each peer hash the copayerId and pass a SIN
to the server. peer discovery is then entirely done using peer's SINs. Note that all copayers in the WR
know the complete copayerIDs of the peers.
The *identity key* is a ECDSA public key derived from peers' extended public key using a specific BIP32
branch. This special public key is never used for Bitcoin address creation, and should only be know by
members of the WR.
In *Copay* this special public key is named *copayerId*. To register into the peerjs server, while not
reveling its copayerId to an entity outside the WR, each peer hashes the copayerId and sends a SIN
to the server. peer discovery is then entirely done using the peer's SINs. Note that all copayers in
the WR know the complete copayerIDs of the peers.
The *network key* is a random key generated when the wallet is created an shared in the initial
'secret string' that peers distribute while the wallet is been created. The network key is then stored
by each peer on the wallet configuration. The network key is used for establishing a CCM/AES
authenticated encrypted channel between all peers, on top of webRTC. The main reason of implementing
by each peer in their wallet configuration. The network key is used for establishing a CCM/AES
authenticated encrypted channel between all peers, on top of webRTC. The main reason for implementing
the *network key* is to prevent man-in-the-middle attacks from a compromised peerjs server.
Secret String
@ -99,7 +98,7 @@ Secret String
When a wallet is created, a secret string is provided to invite new peers to the new wallet. This string
has the following format:
- CopayerId of the peer generating the string. This is a 33 bytes ECDSA public key, as explained above.
- CopayerId of the peer generating the string. This is a 33 byte ECDSA public key, as explained above.
This allow the receiving peer to locate the generating peer.
- Network Key. A 8 byte string to encrypt and sign the peers communication.
@ -109,7 +108,7 @@ Peer authentication
-------------------
It is important to note that all data in the wallet is shared between *all peers*, with the exception of each
peer's private key, with are never transmited throught the network. There is not private messages or, in general,
peer's private key, which are never transmited throught the network. There are no private messages or, in general,
information that belongs to a subset of the WR.

View File

@ -9,18 +9,77 @@ var config = {
//host: 'localhost',
//port: 10009,
//path: '/',
//
//
key: 'g23ihfh82h35rf', // api key for the peerjs server
host:'162.242.219.26', // peerjs server
port:10009,
host: '162.242.219.26', // peerjs server
port: 10009,
path: '/',
maxPeers: 15,
// debug: 3,
debug: 3,
sjclParams: {
salt: 'mjuBtGybi/4=', // choose your own salt (base64)
iter:1000,
mode:'ccm',
ts:parseInt(64),
iter: 1000,
mode: 'ccm',
ts: parseInt(64),
},
config: {
'iceServers': [
// Pass in STUN and TURN servers for maximum network compatibility
{
url: 'stun:stun01.sipphone.com'
}, {
url: 'stun:stun.ekiga.net'
}, {
url: 'stun:stun.fwdnet.net'
}, {
url: 'stun:stun.ideasip.com'
}, {
url: 'stun:stun.iptel.org'
}, {
url: 'stun:stun.rixtelecom.se'
}, {
url: 'stun:stun.schlund.de'
}, {
url: 'stun:stun.l.google.com:19302'
}, {
url: 'stun:stun1.l.google.com:19302'
}, {
url: 'stun:stun2.l.google.com:19302'
}, {
url: 'stun:stun3.l.google.com:19302'
}, {
url: 'stun:stun4.l.google.com:19302'
}, {
url: 'stun:stunserver.org'
}, {
url: 'stun:stun.softjoys.com'
}, {
url: 'stun:stun.voiparound.com'
}, {
url: 'stun:stun.voipbuster.com'
}, {
url: 'stun:stun.voipstunt.com'
}, {
url: 'stun:stun.voxgratia.org'
}, {
url: 'stun:stun.xten.com'
}, {
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc@live.com'
}, {
url: 'turn:192.158.29.39:3478?transport=udp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
}, {
url: 'turn:192.158.29.39:3478?transport=tcp',
credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
username: '28224511:1379330808'
}, {
url: 'turn:homeo@turn.bistri.com:80',
credential: 'homeo'
}
]
}
},
limits: {
@ -47,7 +106,6 @@ var config = {
storageSalt: 'mjuBtGybi/4=', // choose your own salt (base64)
};
var log = function () {
var log = function() {
if (config.verbose) console.log(arguments);
}

View File

@ -267,9 +267,6 @@ hr { margin: 2.25rem 0;}
.lh {line-height: 0;}
.oh {overflow:hidden;}
.lh {line-height: 0;}
.signin input.ng-dirty.ng-invalid {
border: 2px red solid;
}
.video-small {
width: 80px;

View File

@ -168,17 +168,94 @@ button.radius, .button.radius {
border-radius: 5px;
}
button.primary, button.secondary, button.primary:hover, button.secondary:hover {color: #fff !important;}
/* SECONDARY */
button.secondary,
.button.secondary {
background-color: #1ABC9C;
color: #fff;
}
button.secondary:hover,
button.secondary:focus,
.button.secondary:hover,
.button.secondary:focus {
background-color: #16A085;
color: #e6e6e6;
}
button.disabled.secondary,
button[disabled].secondary,
.button.disabled.secondary,
.button[disabled].secondary,
button.disabled.secondary:hover,
button.disabled.secondary:focus,
button[disabled].secondary:hover,
button[disabled].secondary:focus,
.button.disabled.secondary:hover,
.button.disabled.secondary:focus,
.button[disabled].secondary:hover,
.button[disabled].secondary:focus {
background-color: #1ABC9C;
color: #E6E6E6;
}
button.primary { background-color: #E67E22; }
button.secondary { background-color: #1ABC9C !important; }
button.warning { background-color: #C0392A; }
/* PRIMARY */
button.primary,
.button.primary {
background-color: #E67E22;
color: #fff;
}
button.primary:hover,
button.primary:focus,
.button.primary:hover,
.button.primary:focus {
background-color: #D86601;
color: #e6e6e6;
}
button.disabled.primary,
button[disabled].primary,
.button.disabled.primary,
.button[disabled].primary,
button.disabled.primary:hover,
button.disabled.primary:focus,
button[disabled].primary:hover,
button[disabled].primary:focus,
.button.disabled.primary:hover,
.button.disabled.primary:focus,
.button[disabled].primary:hover,
.button[disabled].primary:focus {
background-color: #E67E22;
color: #E6E6E6;
}
button.primary:hover { background-color: #D86601;}
button.secondary:hover { background-color: #16A085 !important;}
button.warning:hover { background-color: #82251A; }
/* WARNING */
button.warning,
.button.warning {
background-color: #C0392A;
color: #fff;
}
button.warning:hover,
button.warning:focus,
.button.warning:hover,
.button.warning:focus {
background-color: #82251A;
color: #e6e6e6;
}
button.disabled.warning,
button[disabled].warning,
.button.disabled.warning,
.button[disabled].warning,
button.disabled.warning:hover,
button.disabled.warning:focus,
button[disabled].warning:hover,
button[disabled].warning:focus,
.button.disabled.warning:hover,
.button.disabled.warning:focus,
.button[disabled].warning:hover,
.button[disabled].warning:focus {
background-color: #C0392A;
color: #E6E6E6;
}
.text-gray { color: #999 !important;}
.text-gray { color: #999;}
#footer {
background: #2C3E50;

View File

@ -143,40 +143,46 @@
</div>
<div ng-show="!loading">
<div class="row">
<div class="large-6 columns">
<div class="box-signin">
<h3>Join a Wallet in Creation</h3>
<input type="text" class="form-control" placeholder="Paste wallet secret here" ng-model="connectionId" required autofocus>
<input type="password" class="form-control" placeholder="Your wallet password" ng-model="joinPassword">
<input type="text" class="form-control" placeholder="Your name (optional)" ng-model="nickname">
<button class="button primary expand radius" ng-click="join()" ng-disabled="loading" loading="Joining">Join</button>
</div>
</div>
<div class="large-6 columns">
<div class="large-6 medium-6 columns">
<div class="box-signin">
<div ng-show="wallets.length">
<h3>Open Wallet</h3>
<select class="form-control" ng-model="selectedWalletId" ng-options="w.id as w.show for w in wallets">
</select>
<input type="password" class="form-control" placeholder="Your wallet password" ng-model="openPassword">
<button class="button secondary expand radius" type="button" ng-click="open()" ng-disabled="loading" loading="Opening">Open</button>
<form name="openForm" ng-submit="open(openForm)" novalidate>
<select class="form-control" ng-model="selectedWalletId" ng-options="w.id as w.show for w in wallets" required>
</select>
<input type="password" class="form-control" placeholder="Your wallet password" name="openPassword" ng-model="openPassword" required>
<button type="submit" class="button secondary radius" ng-disabled="openForm.$invalid || loading" loading="Opening">Open</button>
</form>
</div>
<div ng-show="!wallets.length">
<h3>Create a new wallet</h3>
<input type="text" class="form-control" ng-model="walletName" placeholder="Wallet name (optional)">
<input type="password" class="form-control" placeholder="Your wallet password" ng-model="createPassword">
<button class="button secondary expand radius" ng-click="create()" ng-disabled="loading" loading="Creating">Create</button>
<form name="createForm" ng-submit="create(createForm)" novalidate>
<input type="text" class="form-control" ng-model="walletName" name="walletName" placeholder="Wallet name (optional)">
<input type="password" class="form-control" placeholder="Your wallet password" name="createPassword" ng-model="createPassword" required>
<button type="submit" class="button secondary radius" ng-disabled="createForm.$invalid || loading" loading="Creating">Create</button>
</form>
</div>
</div>
</div>
<div class="large-6 medium-6 columns">
<div class="box-signin">
<h3>Join a Wallet in Creation</h3>
<form name="joinForm" ng-submit="join(joinForm)" novalidate>
<input type="text" class="form-control" placeholder="Paste wallet secret here" name="connectionId" ng-model="connectionId" required>
<input type="password" class="form-control" placeholder="Your wallet password" name="joinPassword" ng-model="joinPassword" required>
<input type="text" class="form-control" placeholder="Your name (optional)" name="nickname" ng-model="nickname">
<button type="submit" class="button primary radius" ng-disabled="joinForm.$invalid || loading" loading="Joining">Join</button>
</form>
</div>
</div>
</div>
<div class="row">
<div class="large-12 columns text-center line-dashed">
<span ng-show="wallets.length">
<a ng-click="create()" ng-disabled="loading" loading="Creating">Create a new wallet</a> &middot;
<a href="#/setup">Create a new wallet</a> &middot;
</span>
<a ng-href="#import">Import from file</a>
<a ng-href="#import">Import a backup</a>
</div>
</div>
</div> <!-- End !loading -->
@ -185,14 +191,23 @@
<script type="text/ng-template" id="import.html">
<div ng-controller="ImportController">
<h3>{{title}}</h3>
<div class="large-6 columns">
<input type="password" class="form-control" placeholder="Your wallet password" ng-model="password" autofocus required>
<h6>Select a backup file</h6>
<input type="file" class="form-control" placeholder="Select a backup file" ng-model="backupFile" ng-file-select>
<h6>Or just paste the backup text here</h6>
<textarea class="form-control" ng-model="backupText"></textarea>
<button class="button primary expand radius" ng-click="import()">Import backup</button>
<div class="large-6 large-centered medium-6 medium-centered columns" ng-init="choosefile=0; pastetext=0">
<h2>{{title}}</h2>
<form name="importForm" ng-submit="import(importForm)" novalidate>
<fieldset>
<legend>Select which method want to use to restore</legend>
<label for="backupFile" ng-click="choosefile=!choosefile" class="m10b"><i class="fi-upload"></i> Choose backup file from your computer</label>
<input type="file" class="form-control" placeholder="Select a backup file" name="backupFile" ng-model="backupFile" ng-file-select ng-show="choosefile">
<label for="backupText" ng-click="pastetext=!pastetext" class="m10b"><i class="fi-paperclip"></i> Paste backup plain text code</label>
<textarea class="form-control" name="backupText" ng-model="backupText" rows="5" ng-show="pastetext"></textarea>
</fieldset>
<label for="password">Password <small>Required</small></label>
<input type="password" class="form-control" placeholder="Your wallet password" name="password" ng-model="password" required>
<button type="submit" class="button primary radius" ng-disabled="importForm.$invalid" loading="Importing">
Import backup
</button>
</form>
</div>
</div>
</script>
@ -203,6 +218,7 @@
Creating new wallet...
</div>
<div ng-show="!loading">
<form name="setupForm" ng-submit="create(setupForm)" novalidate>
<div class="row">
<div class="small-12 medium-8 medium-centered large-8 large-centered columns box-setup">
<div class="large-6 columns line-dashed-v">
@ -226,27 +242,27 @@
</div>
</div>
<div class="small-12 medium-6 medium-centered large-6 large-centered columns m30v">
<h6>Wallet Password</h6>
<h6>Wallet Password <small>Required</small></h6>
<input type="password" class="form-control" ng-model="walletPassword" required>
</div>
<div class="small-12 medium-6 medium-centered large-6 large-centered columns m30v">
<h6>Wallet name <small>(optional)</small></h6>
<h6>Wallet name <small>Optional</small></h6>
<input type="text" class="form-control" ng-model="walletName">
</div>
<div class="large-6 large-centered columns m30v">
<h6>Your name <small>(optional)</small></h6>
<h6>Your name <small>Optional</small></h6>
<input type="text" class="form-control" ng-model="myNickname">
</div>
</div>
<div class="row">
<div class="large-12 columns line-dashed">
<button class="button primary radius right" type="button"
ng-click="create()">
<button type="submit" class="button primary radius right" ng-disabled="setupForm.$invalid || loading">
Create {{requiredCopayers}}-of-{{totalCopayers}} wallet
</button>
<a class="button secondary radius" href="#signin">Go back</a>
</div>
</div>
</form>
</div>
</div>
</script>

View File

@ -44,6 +44,7 @@ angular.module('copay.header').controller('HeaderController',
};
$scope.refresh = function() {
var w = $rootScope.wallet;
controllerUtils.updateBalance(function() {
w.connectToAll();
$rootScope.$digest();

View File

@ -21,13 +21,31 @@ angular.module('copay.import').controller('ImportController',
};
};
$scope.import = function() {
if ($scope.password) {
if ($scope.backupText) {
_importBackup($scope.backupText);
} else {
reader.readAsBinaryString($scope.file);
}
$scope.import = function(form) {
if (form.$invalid) {
$rootScope.flashMessage = { message: 'There is an error in the form. Please, try again', type: 'error'};
return;
}
var backupFile = $scope.file;
var backupText = form.backupText.$modelValue;
var password = form.password.$modelValue;
if (!backupFile && !backupText) {
$rootScope.flashMessage = { message: 'Please, select your backup file or paste the text', type: 'error'};
return;
}
$scope.loading = true;
if (backupFile) {
reader.readAsBinaryString(backupFile);
}
else {
_importBackup(backupText);
}
$scope.loading = false;
};
});

View File

@ -32,7 +32,12 @@ angular.module('copay.setup').controller('SetupController',
updateRCSelect(tc);
});
$scope.create = function() {
$scope.create = function(form) {
if (form && form.$invalid) {
$rootScope.flashMessage = { message: 'Please, enter required fields', type: 'error'};
return;
}
$scope.loading = true;
var passphrase = Passphrase.getBase64($scope.walletPassword);

View File

@ -7,30 +7,40 @@ angular.module('copay.signin').controller('SigninController',
$scope.selectedWalletId = $scope.wallets.length ? $scope.wallets[0].id : null;
$scope.openPassword = '';
$scope.create = function() {
$scope.loading = true;
$scope.create = function(form) {
if (form && form.$invalid) {
$rootScope.flashMessage = { message: 'Please, enter required fields', type: 'error'};
return;
}
$rootScope.walletName = $scope.walletName;
$rootScope.walletPassword = $scope.createPassword;
$rootScope.walletName = form.walletName.$modelValue;
$rootScope.walletPassword = form.createPassword.$modelValue;
$location.path('setup');
};
$scope.open = function() {
if ($scope.openPassword != '') {
$scope.loading = true;
var passphrase = Passphrase.getBase64($scope.openPassword);
var w = walletFactory.open($scope.selectedWalletId, { passphrase: passphrase});
if (!w) {
$scope.loading = false;
$rootScope.flashMessage = { message: 'Bad password or connection error', type: 'error'};
return;
}
controllerUtils.startNetwork(w);
$scope.open = function(form) {
if (form && form.$invalid) {
$rootScope.flashMessage = { message: 'Please, enter required fields', type: 'error'};
return;
}
$scope.loading = true;
var password = form.openPassword.$modelValue;
var passphrase = Passphrase.getBase64(password);
var w = walletFactory.open($scope.selectedWalletId, { passphrase: passphrase});
if (!w) {
$scope.loading = false;
$rootScope.flashMessage = { message: 'Bad password or connection error', type: 'error'};
return;
}
controllerUtils.startNetwork(w);
};
$scope.join = function() {
$scope.join = function(form) {
if (form && form.$invalid) {
$rootScope.flashMessage = { message: 'Please, enter required fields', type: 'error'};
return;
}
$scope.loading = true;
walletFactory.network.on('badSecret', function() {

View File

@ -65,6 +65,8 @@ WalletFactory.prototype.fromObj = function(obj) {
}
this.log('### WALLET OPENED:', w.id);
// store imported wallet
w.store();
return w;
};

View File

@ -28,11 +28,8 @@ function Network(opts) {
mode:'ccm',
ts:parseInt(64),
};
// For using your own peerJs server
self.opts = {};
['port', 'host', 'path', 'debug', 'key'].forEach(function(k) {
this.opts = {};
['config', 'port', 'host', 'path', 'debug', 'key'].forEach(function(k) {
if (opts[k]) self.opts[k] = opts[k];
});
this.cleanUp();

View File

@ -30,7 +30,7 @@ var createBundle = function(opts) {
opts.dir = opts.dir || 'js/';
// concat browser vendor files
exec('cd ' + opts.dir + 'browser; sh concat.sh', puts);
exec('sh concat.sh', puts);
var bopts = {
pack: pack,