Publish Vuvuzela v0.1
This commit is contained in:
parent
d54f8deb74
commit
cf61af556e
|
@ -0,0 +1,15 @@
|
|||
Vuvuzela: Scalable Private Messaging
|
||||
Copyright (C) 2015 David Lazar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
119
README.md
119
README.md
|
@ -1,6 +1,117 @@
|
|||
> *Metadata absolutely tells you everything about somebody's life*
|
||||
>
|
||||
> — [Stewart Baker](http://www.nybooks.com/articles/archives/2013/nov/21/snowden-leaks-and-public/), former General Counsel of the NSA
|
||||
|
||||
|
|
||||
|
||||
> *We kill people based on metadata*
|
||||
>
|
||||
> — [Michael Hayden](https://www.youtube.com/watch?v=kV2HDM86XgI&t=17m53s), former Director of the NSA
|
||||
|
||||
# Vuvuzela
|
||||
|
||||
Vuvuzela is a scalable private messaging system that hides metadata.
|
||||
The code will be available soon, but please read our
|
||||
[SOSP paper](http://sigops.org/sosp/sosp15/current/2015-Monterey/printable/136-hooff.pdf)
|
||||
in the meantime.
|
||||
Vuvuzela is a messaging system that protects the privacy of message contents
|
||||
and message metadata. Users communicating through Vuvuzela do not reveal who
|
||||
they are talking to, even in the presence of powerful nation-state adversaries.
|
||||
Our [SOSP 2015 paper](https://davidlazar.org/papers/vuvuzela.pdf) explains
|
||||
the system, its threat model, performance, limitations, and more.
|
||||
|
||||
Vuvuzela is the first system that provides strong metadata privacy while
|
||||
scaling to millions of users. Previous systems that hide metadata using
|
||||
Tor (such as [Pond](https://pond.imperialviolet.org/)) are prone to traffic
|
||||
analysis attacks. Systems that encrypt metadata using techniques like
|
||||
DC-nets and PIR don't scale past thousands of users.
|
||||
|
||||
Vuvuzela uses efficient cryptography ([NaCl](http://nacl.cr.yp.to)) to hide as
|
||||
much metadata as possible and adds noise to metadata that can't be encrypted
|
||||
efficiently. This approach provides less privacy than encrypting all of the
|
||||
metadata, but it enables Vuvuzela to support millions of users. Nonetheless,
|
||||
Vuvuzela adds enough noise to thwart adversaries like the NSA and guarantees
|
||||
[differential privacy](https://en.wikipedia.org/wiki/Differential_privacy) for
|
||||
users' metadata.
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
**A conversation in the Vuvuzela client**
|
||||
|
||||
![client](https://github.com/davidlazar/vuvuzela/blob/master/screenshots/client.gif)
|
||||
|
||||
In practice, the message latency would be around 20s to 40s, depending
|
||||
on security parameters and the number of users connected to the system.
|
||||
|
||||
**Noise generated by the Vuvuzela servers**
|
||||
|
||||
![server](https://github.com/davidlazar/vuvuzela/blob/master/screenshots/server.gif)
|
||||
|
||||
Vuvuzela is unable to encrypt two kinds of metadata: the number of idle users
|
||||
(connected users without a conversation partner) and the number of active users
|
||||
(users engaged in a conversation). Without noise, a sophisticated adversary
|
||||
could use this metadata to learn who is talking to who. However, the Vuvuzela
|
||||
servers generate noise that perturbs this metadata so that it is difficult to
|
||||
exploit.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Follow these steps to run the Vuvuzela system locally using the provided
|
||||
sample configs.
|
||||
|
||||
1. Install Vuvuzela (assuming `GOPATH=~/go`):
|
||||
|
||||
$ go get github.com/davidlazar/vuvuzela/...
|
||||
|
||||
The remaining steps assume `PATH` contains `~/go/bin` and that the
|
||||
current working directory is `~/go/src/github.com/davidlazar/vuvuzela`.
|
||||
|
||||
2. Start the last Vuvuzela server:
|
||||
|
||||
$ vuvuzela-server -conf confs/local-last.conf
|
||||
|
||||
3. Start the middle server (in a new shell):
|
||||
|
||||
$ vuvuzela-server -conf confs/local-middle.conf
|
||||
|
||||
4. Start the first server (in a new shell):
|
||||
|
||||
$ vuvuzela-server -conf confs/local-first.conf
|
||||
|
||||
5. Start the entry server (in a new shell):
|
||||
|
||||
$ vuvuzela-entry-server -wait 1s
|
||||
|
||||
6. Run the Vuvuzela client:
|
||||
|
||||
$ vuvuzela-client -conf confs/alice.conf
|
||||
|
||||
The client supports these commands:
|
||||
|
||||
* `/dial <user>` to dial another user
|
||||
* `/talk <user>` to start a conversation
|
||||
* `/talk <yourself>` to end a conversation
|
||||
|
||||
|
||||
## Deployment considerations
|
||||
|
||||
This Vuvuzela implementation is not ready for wide-use deployment.
|
||||
In particular, we haven't yet implemented these crucial components:
|
||||
|
||||
* **Public Key Infrastructure**:
|
||||
Vuvuzela assumes the existence of a PKI in which users can privately
|
||||
learn each others public keys. This implementation uses `pki.conf`
|
||||
as a placeholder until we integrate a real PKI.
|
||||
|
||||
* **CDN to distribute dialing dead drops**:
|
||||
Vuvuzela's dialing protocol (used to initiate conversations) uses a
|
||||
lot of server bandwidth. To make dialing practical, Vuvuzela should
|
||||
use a CDN or BitTorrent to distribute the dialing dead drops.
|
||||
|
||||
There is a lot more interesting work to do. See the
|
||||
[issue tracker](https://github.com/davidlazar/vuvuzela/issues)
|
||||
for more information.
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This code is written by David Lazar with contributions from
|
||||
Jelle van den Hooff, Nickolai Zeldovich, and Matei Zaharia.
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/davidlazar/go-crypto/encoding/base32"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
type BoxKey [32]byte
|
||||
|
||||
func GenerateBoxKey(rand io.Reader) (publicKey, privateKey *BoxKey, err error) {
|
||||
pub, priv, err := box.GenerateKey(rand)
|
||||
return (*BoxKey)(pub), (*BoxKey)(priv), err
|
||||
}
|
||||
|
||||
func (k *BoxKey) Key() *[32]byte {
|
||||
return (*[32]byte)(k)
|
||||
}
|
||||
|
||||
func (k *BoxKey) String() string {
|
||||
return base32.EncodeToString(k[:])
|
||||
}
|
||||
|
||||
func KeyFromString(s string) (*BoxKey, error) {
|
||||
key := new(BoxKey)
|
||||
b, err := base32.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base32 decode error: %s", err)
|
||||
}
|
||||
if copy(key[:], b) < 32 {
|
||||
return nil, fmt.Errorf("short key")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (k *BoxKey) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(k.String())
|
||||
}
|
||||
|
||||
func (k *BoxKey) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
bs, err := base32.DecodeString(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("base32 decode error: %s", err)
|
||||
}
|
||||
if copy(k[:], bs) < 32 {
|
||||
return fmt.Errorf("short key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type BoxKeys []*BoxKey
|
||||
|
||||
func (keys BoxKeys) Keys() []*[32]byte {
|
||||
xs := make([]*[32]byte, len(keys))
|
||||
for i := range keys {
|
||||
xs[i] = (*[32]byte)(keys[i])
|
||||
}
|
||||
return xs
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MyName": "alice",
|
||||
"MyPublicKey": "j10hpqtgnqc1y21xp5y7yamwa32jvdp89888q2semnxg95j4v82g",
|
||||
"MyPrivateKey": "82v7008ke1dyzatzq04mrtnxt5s92vnfxpdgr61rbtw30hbge330"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"MyName": "bob",
|
||||
"MyPublicKey": "nhd9ja88j65zwmnszw0b12zg1wqgqqmq382tafyw3gd642e9s1ag",
|
||||
"MyPrivateKey": "wqdzrmdyvk7ee8w37r8ey56pt5dkkfz64039qhzv3w81a75bgsc0"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ServerName": "local-first",
|
||||
"DebugAddr": ":12718",
|
||||
"PublicKey": "pd04y1ryrfxtrayjg9f4cfsw1ayfhwrcfd7g7emhfjrsc4cd20f0",
|
||||
"PrivateKey": "v5sr0d6d2efr3hrbfw5qxxsnvhqh44kkqed1f43txe4qr8rhk310",
|
||||
"ConvoMu": 1000.0,
|
||||
"ConvoB": 4.0,
|
||||
"DialMu": 100.0,
|
||||
"DialB": 4.0
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"ServerName": "local-last",
|
||||
"ListenAddr": ":2720",
|
||||
"DebugAddr": ":12719",
|
||||
"PublicKey": "fkaf8ds0a4fmdsztqzpcn4em9npyv722bxv2683n9fdydzdjwgy0",
|
||||
"PrivateKey": "bvypy8wgg8a5tag3zw8r4atx8e31qcdrqxvveaz5cdv46s5sjyb0",
|
||||
"ConvoMu": 1000.0,
|
||||
"ConvoB": 4.0,
|
||||
"DialMu": 100.0,
|
||||
"DialB": 4.0
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"ServerName": "local-middle",
|
||||
"ListenAddr": ":2719",
|
||||
"PublicKey": "349bs143gvm7n0kxwhsaayeta2ptjrybwf37s4j7sj0yfrc3dxs0",
|
||||
"PrivateKey": "c7g9y76ehpc90w3a9t541705enragpzg6p588b5xn8pnvk0a5h50",
|
||||
"ConvoMu": 1000.0,
|
||||
"ConvoB": 4.0,
|
||||
"DialMu": 100.0,
|
||||
"DialB": 4.0
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"People": {
|
||||
"alice": "j10hpqtgnqc1y21xp5y7yamwa32jvdp89888q2semnxg95j4v82g",
|
||||
"bob": "nhd9ja88j65zwmnszw0b12zg1wqgqqmq382tafyw3gd642e9s1ag"
|
||||
},
|
||||
"Servers": {
|
||||
"local-first": {
|
||||
"Address": "localhost",
|
||||
"PublicKey": "pd04y1ryrfxtrayjg9f4cfsw1ayfhwrcfd7g7emhfjrsc4cd20f0"
|
||||
},
|
||||
"local-middle": {
|
||||
"Address": "localhost:2719",
|
||||
"PublicKey": "349bs143gvm7n0kxwhsaayeta2ptjrybwf37s4j7sj0yfrc3dxs0"
|
||||
},
|
||||
"local-last": {
|
||||
"Address": "localhost:2720",
|
||||
"PublicKey": "fkaf8ds0a4fmdsztqzpcn4em9npyv722bxv2683n9fdydzdjwgy0"
|
||||
}
|
||||
},
|
||||
"ServerOrder": ["local-first", "local-middle", "local-last"],
|
||||
"EntryServer": "ws://localhost:8080"
|
||||
}
|
|
@ -0,0 +1,425 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
"github.com/davidlazar/vuvuzela/vrpc"
|
||||
)
|
||||
|
||||
type ConvoService struct {
|
||||
roundsMu sync.RWMutex
|
||||
rounds map[uint32]*ConvoRound
|
||||
|
||||
Idle *sync.Mutex
|
||||
|
||||
LaplaceMu float64
|
||||
LaplaceB float64
|
||||
|
||||
PKI *PKI
|
||||
ServerName string
|
||||
PrivateKey *BoxKey
|
||||
Client *vrpc.Client
|
||||
LastServer bool
|
||||
|
||||
AccessCounts chan *AccessCount
|
||||
}
|
||||
|
||||
type ConvoRound struct {
|
||||
srv *ConvoService
|
||||
status convoStatus
|
||||
|
||||
numIncoming int
|
||||
sharedKeys []*[32]byte
|
||||
incoming [][]byte
|
||||
incomingIndex []int
|
||||
|
||||
replies [][]byte
|
||||
|
||||
numFakeSingles int
|
||||
numFakeDoubles int
|
||||
|
||||
noise [][]byte
|
||||
noiseWg sync.WaitGroup
|
||||
}
|
||||
|
||||
type convoStatus int
|
||||
|
||||
const (
|
||||
convoRoundNew convoStatus = iota + 1
|
||||
convoRoundOpen
|
||||
convoRoundClosed
|
||||
)
|
||||
|
||||
type AccessCount struct {
|
||||
Singles int64
|
||||
Doubles int64
|
||||
}
|
||||
|
||||
func InitConvoService(srv *ConvoService) {
|
||||
srv.rounds = make(map[uint32]*ConvoRound)
|
||||
srv.AccessCounts = make(chan *AccessCount, 8)
|
||||
}
|
||||
|
||||
func (srv *ConvoService) getRound(round uint32, expectedStatus convoStatus) (*ConvoRound, error) {
|
||||
srv.roundsMu.RLock()
|
||||
r, ok := srv.rounds[round]
|
||||
srv.roundsMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("round %d not found", round)
|
||||
}
|
||||
if r.status != expectedStatus {
|
||||
return r, fmt.Errorf("round %d: status %v, expecting %v", round, r.status, expectedStatus)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (srv *ConvoService) NewRound(Round uint32, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "NewRound", "round": Round}).Info()
|
||||
|
||||
// wait for the service to become idle before starting a new round
|
||||
// TODO temporary hack
|
||||
srv.Idle.Lock()
|
||||
|
||||
srv.roundsMu.Lock()
|
||||
defer srv.roundsMu.Unlock()
|
||||
|
||||
_, exists := srv.rounds[Round]
|
||||
if exists {
|
||||
return fmt.Errorf("round %d already exists", Round)
|
||||
}
|
||||
|
||||
round := &ConvoRound{
|
||||
srv: srv,
|
||||
}
|
||||
srv.rounds[Round] = round
|
||||
|
||||
if !srv.LastServer {
|
||||
round.numFakeSingles = cappedFlooredLaplace(srv.LaplaceMu, srv.LaplaceB)
|
||||
round.numFakeDoubles = cappedFlooredLaplace(srv.LaplaceMu, srv.LaplaceB)
|
||||
round.numFakeDoubles += round.numFakeDoubles % 2 // ensure numFakeDoubles is even
|
||||
round.noise = make([][]byte, round.numFakeSingles+round.numFakeDoubles)
|
||||
|
||||
nonce := ForwardNonce(Round)
|
||||
nextKeys := srv.PKI.NextServerKeys(srv.ServerName).Keys()
|
||||
round.noiseWg.Add(1)
|
||||
go func() {
|
||||
FillWithFakeSingles(round.noise[:round.numFakeSingles], nonce, nextKeys)
|
||||
FillWithFakeDoubles(round.noise[round.numFakeSingles:], nonce, nextKeys)
|
||||
round.noiseWg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
round.status = convoRoundNew
|
||||
return nil
|
||||
}
|
||||
|
||||
type ConvoOpenArgs struct {
|
||||
Round uint32
|
||||
NumIncoming int
|
||||
}
|
||||
|
||||
func (srv *ConvoService) Open(args *ConvoOpenArgs, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "Open", "round": args.Round, "incoming": args.NumIncoming}).Info()
|
||||
|
||||
round, err := srv.getRound(args.Round, convoRoundNew)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
round.numIncoming = args.NumIncoming
|
||||
round.sharedKeys = make([]*[32]byte, round.numIncoming)
|
||||
round.incoming = make([][]byte, round.numIncoming)
|
||||
round.status = convoRoundOpen
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ConvoAddArgs struct {
|
||||
Round uint32
|
||||
Offset int
|
||||
Onions [][]byte
|
||||
}
|
||||
|
||||
func (srv *ConvoService) Add(args *ConvoAddArgs, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "Add", "round": args.Round, "onions": len(args.Onions)}).Debug()
|
||||
|
||||
round, err := srv.getRound(args.Round, convoRoundOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonce := ForwardNonce(args.Round)
|
||||
expectedOnionSize := srv.PKI.IncomingOnionOverhead(srv.ServerName) + SizeConvoExchange
|
||||
|
||||
if args.Offset+len(args.Onions) > round.numIncoming {
|
||||
return fmt.Errorf("overflowing onions (offset=%d, onions=%d, incoming=%d)", args.Offset, len(args.Onions), round.numIncoming)
|
||||
}
|
||||
|
||||
for k, onion := range args.Onions {
|
||||
i := args.Offset + k
|
||||
round.sharedKeys[i] = new([32]byte)
|
||||
|
||||
if len(onion) == expectedOnionSize {
|
||||
var theirPublic [32]byte
|
||||
copy(theirPublic[:], onion[0:32])
|
||||
|
||||
box.Precompute(round.sharedKeys[i], &theirPublic, srv.PrivateKey.Key())
|
||||
|
||||
message, ok := box.OpenAfterPrecomputation(nil, onion[32:], nonce, round.sharedKeys[i])
|
||||
if ok {
|
||||
round.incoming[i] = message
|
||||
}
|
||||
} else {
|
||||
// for debugging
|
||||
log.WithFields(log.Fields{"round": args.Round, "offset": args.Offset, "onions": len(args.Onions), "onion": k, "onionLen": len(onion)}).Error("bad onion size")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *ConvoService) filterIncoming(round *ConvoRound) {
|
||||
incomingValid := make([][]byte, len(round.incoming))
|
||||
incomingIndex := make([]int, len(round.incoming))
|
||||
|
||||
seen := make(map[uint64]bool)
|
||||
v := 0
|
||||
for i, msg := range round.incoming {
|
||||
if msg == nil {
|
||||
incomingIndex[i] = -1
|
||||
continue
|
||||
}
|
||||
msgkey := binary.BigEndian.Uint64(msg[len(msg)-8:])
|
||||
if seen[msgkey] {
|
||||
incomingIndex[i] = -1
|
||||
} else {
|
||||
seen[msgkey] = true
|
||||
incomingValid[v] = msg
|
||||
incomingIndex[i] = v
|
||||
v++
|
||||
}
|
||||
}
|
||||
|
||||
round.incoming = incomingValid[:v]
|
||||
round.incomingIndex = incomingIndex
|
||||
}
|
||||
|
||||
func (srv *ConvoService) Close(Round uint32, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "Close", "round": Round}).Info()
|
||||
|
||||
round, err := srv.getRound(Round, convoRoundOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.filterIncoming(round)
|
||||
|
||||
if !srv.LastServer {
|
||||
round.noiseWg.Wait()
|
||||
|
||||
outgoing := append(round.incoming, round.noise...)
|
||||
round.noise = nil
|
||||
|
||||
shuffler := NewShuffler(rand.Reader, len(outgoing))
|
||||
shuffler.Shuffle(outgoing)
|
||||
|
||||
if err := NewConvoRound(srv.Client, Round); err != nil {
|
||||
return fmt.Errorf("NewConvoRound: %s", err)
|
||||
}
|
||||
srv.Idle.Unlock()
|
||||
|
||||
replies, err := RunConvoRound(srv.Client, Round, outgoing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("RunConvoRound: %s", err)
|
||||
}
|
||||
|
||||
shuffler.Unshuffle(replies)
|
||||
round.replies = replies[:round.numIncoming]
|
||||
} else {
|
||||
exchanges := make([]*ConvoExchange, len(round.incoming))
|
||||
ParallelFor(len(round.incoming), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
exchanges[i] = new(ConvoExchange)
|
||||
if err := exchanges[i].Unmarshal(round.incoming[i]); err != nil {
|
||||
log.WithFields(log.Fields{"bug": true, "call": "ConvoExchange.Unmarshal"}).Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var singles, doubles int64
|
||||
deadDrops := make(map[DeadDrop][]int)
|
||||
for i, ex := range exchanges {
|
||||
drop := deadDrops[ex.DeadDrop]
|
||||
if len(drop) == 0 {
|
||||
singles++
|
||||
deadDrops[ex.DeadDrop] = append(drop, i)
|
||||
} else if len(drop) == 1 {
|
||||
singles--
|
||||
doubles++
|
||||
deadDrops[ex.DeadDrop] = append(drop, i)
|
||||
}
|
||||
}
|
||||
|
||||
round.replies = make([][]byte, len(round.incoming))
|
||||
ParallelFor(len(exchanges), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
ex := exchanges[i]
|
||||
drop := deadDrops[ex.DeadDrop]
|
||||
if len(drop) == 1 {
|
||||
round.replies[i] = ex.EncryptedMessage[:]
|
||||
}
|
||||
if len(drop) == 2 {
|
||||
var k int
|
||||
if i == drop[0] {
|
||||
k = drop[1]
|
||||
} else {
|
||||
k = drop[0]
|
||||
}
|
||||
round.replies[i] = exchanges[k].EncryptedMessage[:]
|
||||
}
|
||||
}
|
||||
})
|
||||
srv.Idle.Unlock()
|
||||
|
||||
ac := &AccessCount{
|
||||
Singles: singles,
|
||||
Doubles: doubles,
|
||||
}
|
||||
select {
|
||||
case srv.AccessCounts <- ac:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
round.status = convoRoundClosed
|
||||
return nil
|
||||
}
|
||||
|
||||
type ConvoGetArgs struct {
|
||||
Round uint32
|
||||
Offset int
|
||||
Count int
|
||||
}
|
||||
|
||||
type ConvoGetResult struct {
|
||||
Onions [][]byte
|
||||
}
|
||||
|
||||
func (srv *ConvoService) Get(args *ConvoGetArgs, result *ConvoGetResult) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "Get", "round": args.Round, "count": args.Count}).Debug()
|
||||
|
||||
round, err := srv.getRound(args.Round, convoRoundClosed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonce := BackwardNonce(args.Round)
|
||||
outgoingOnionSize := srv.PKI.OutgoingOnionOverhead(srv.ServerName) + SizeEncryptedMessage
|
||||
|
||||
result.Onions = make([][]byte, args.Count)
|
||||
for k := range result.Onions {
|
||||
i := args.Offset + k
|
||||
|
||||
if v := round.incomingIndex[i]; v > -1 {
|
||||
reply := round.replies[v]
|
||||
onion := box.SealAfterPrecomputation(nil, reply, nonce, round.sharedKeys[i])
|
||||
result.Onions[k] = onion
|
||||
}
|
||||
if len(result.Onions[k]) != outgoingOnionSize {
|
||||
onion := make([]byte, outgoingOnionSize)
|
||||
rand.Read(onion)
|
||||
result.Onions[k] = onion
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *ConvoService) Delete(Round uint32, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "convo", "rpc": "Delete", "round": Round}).Info()
|
||||
|
||||
srv.roundsMu.Lock()
|
||||
delete(srv.rounds, Round)
|
||||
srv.roundsMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConvoRound(client *vrpc.Client, round uint32) error {
|
||||
return client.Call("ConvoService.NewRound", round, nil)
|
||||
}
|
||||
|
||||
func RunConvoRound(client *vrpc.Client, round uint32, onions [][]byte) ([][]byte, error) {
|
||||
openArgs := &ConvoOpenArgs{
|
||||
Round: round,
|
||||
NumIncoming: len(onions),
|
||||
}
|
||||
if err := client.Call("ConvoService.Open", openArgs, nil); err != nil {
|
||||
return nil, fmt.Errorf("Open: %s", err)
|
||||
}
|
||||
|
||||
spans := Spans(len(onions), 4000)
|
||||
calls := make([]*vrpc.Call, len(spans))
|
||||
|
||||
ParallelFor(len(calls), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
span := spans[i]
|
||||
calls[i] = &vrpc.Call{
|
||||
Method: "ConvoService.Add",
|
||||
Args: &ConvoAddArgs{
|
||||
Round: round,
|
||||
Offset: span.Start,
|
||||
Onions: onions[span.Start : span.Start+span.Count],
|
||||
},
|
||||
Reply: nil,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := client.CallMany(calls); err != nil {
|
||||
return nil, fmt.Errorf("Add: %s", err)
|
||||
}
|
||||
|
||||
if err := client.Call("ConvoService.Close", round, nil); err != nil {
|
||||
return nil, fmt.Errorf("Close: %s", err)
|
||||
}
|
||||
|
||||
ParallelFor(len(calls), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
span := spans[i]
|
||||
calls[i] = &vrpc.Call{
|
||||
Method: "ConvoService.Get",
|
||||
Args: &ConvoGetArgs{
|
||||
Round: round,
|
||||
Offset: span.Start,
|
||||
Count: span.Count,
|
||||
},
|
||||
Reply: new(ConvoGetResult),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := client.CallMany(calls); err != nil {
|
||||
return nil, fmt.Errorf("Get: %s", err)
|
||||
}
|
||||
|
||||
replies := make([][]byte, len(onions))
|
||||
ParallelFor(len(calls), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
span := spans[i]
|
||||
copy(replies[span.Start:span.Start+span.Count], calls[i].Reply.(*ConvoGetResult).Onions)
|
||||
}
|
||||
})
|
||||
|
||||
if err := client.Call("ConvoService.Delete", round, nil); err != nil {
|
||||
return nil, fmt.Errorf("Delete: %s", err)
|
||||
}
|
||||
|
||||
return replies, nil
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
"github.com/davidlazar/vuvuzela/vrpc"
|
||||
)
|
||||
|
||||
type DialService struct {
|
||||
roundsMu sync.RWMutex
|
||||
rounds map[uint32]*DialRound
|
||||
|
||||
Idle *sync.Mutex
|
||||
|
||||
LaplaceMu float64
|
||||
LaplaceB float64
|
||||
|
||||
PKI *PKI
|
||||
ServerName string
|
||||
PrivateKey *BoxKey
|
||||
Client *vrpc.Client
|
||||
LastServer bool
|
||||
}
|
||||
|
||||
type DialRound struct {
|
||||
sync.Mutex
|
||||
|
||||
status dialStatus
|
||||
incoming [][]byte
|
||||
|
||||
noise [][]byte
|
||||
noiseWg sync.WaitGroup
|
||||
}
|
||||
|
||||
type dialStatus int
|
||||
|
||||
const (
|
||||
dialRoundOpen dialStatus = iota + 1
|
||||
dialRoundClosed
|
||||
)
|
||||
|
||||
func InitDialService(srv *DialService) {
|
||||
srv.rounds = make(map[uint32]*DialRound)
|
||||
}
|
||||
|
||||
func (srv *DialService) getRound(round uint32, expectedStatus dialStatus) (*DialRound, error) {
|
||||
srv.roundsMu.RLock()
|
||||
r, ok := srv.rounds[round]
|
||||
srv.roundsMu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("round %d not found", round)
|
||||
}
|
||||
if r.status != expectedStatus {
|
||||
return r, fmt.Errorf("round %d: status %v, expecting %v", round, r.status, expectedStatus)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (srv *DialService) NewRound(Round uint32, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "dial", "rpc": "NewRound", "round": Round}).Info()
|
||||
srv.Idle.Lock()
|
||||
|
||||
srv.roundsMu.Lock()
|
||||
defer srv.roundsMu.Unlock()
|
||||
|
||||
_, exists := srv.rounds[Round]
|
||||
if exists {
|
||||
return fmt.Errorf("round %d already exists", Round)
|
||||
}
|
||||
|
||||
round := &DialRound{}
|
||||
srv.rounds[Round] = round
|
||||
|
||||
round.noiseWg.Add(1)
|
||||
go func() {
|
||||
// NOTE: unlike the convo protocol, the last server also adds noise
|
||||
noiseTotal := 0
|
||||
noiseCounts := make([]int, TotalDialBuckets)
|
||||
for b := range noiseCounts {
|
||||
bmu := cappedFlooredLaplace(srv.LaplaceMu, srv.LaplaceB)
|
||||
noiseCounts[b] = bmu
|
||||
noiseTotal += bmu
|
||||
}
|
||||
round.noise = make([][]byte, noiseTotal)
|
||||
|
||||
nonce := ForwardNonce(Round)
|
||||
nextKeys := srv.PKI.NextServerKeys(srv.ServerName).Keys()
|
||||
|
||||
FillWithFakeIntroductions(round.noise, noiseCounts, nonce, nextKeys)
|
||||
round.noiseWg.Done()
|
||||
}()
|
||||
|
||||
round.status = dialRoundOpen
|
||||
return nil
|
||||
}
|
||||
|
||||
type DialAddArgs struct {
|
||||
Round uint32
|
||||
Onions [][]byte
|
||||
}
|
||||
|
||||
func (srv *DialService) Add(args *DialAddArgs, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "dial", "rpc": "Add", "round": args.Round, "onions": len(args.Onions)}).Debug()
|
||||
|
||||
round, err := srv.getRound(args.Round, dialRoundOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nonce := ForwardNonce(args.Round)
|
||||
messages := make([][]byte, 0, len(args.Onions))
|
||||
expectedOnionSize := srv.PKI.IncomingOnionOverhead(srv.ServerName) + SizeDialExchange
|
||||
|
||||
for _, onion := range args.Onions {
|
||||
if len(onion) == expectedOnionSize {
|
||||
var theirPublic [32]byte
|
||||
copy(theirPublic[:], onion[0:32])
|
||||
|
||||
message, ok := box.Open(nil, onion[32:], nonce, &theirPublic, srv.PrivateKey.Key())
|
||||
if ok {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
round.Lock()
|
||||
round.incoming = append(round.incoming, messages...)
|
||||
round.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *DialService) filterIncoming(round *DialRound) {
|
||||
incomingValid := make([][]byte, 0, len(round.incoming))
|
||||
|
||||
seen := make(map[uint64]bool)
|
||||
for _, msg := range round.incoming {
|
||||
msgkey := binary.BigEndian.Uint64(msg[len(msg)-8:])
|
||||
if !seen[msgkey] {
|
||||
seen[msgkey] = true
|
||||
incomingValid = append(incomingValid, msg)
|
||||
}
|
||||
}
|
||||
|
||||
round.incoming = incomingValid
|
||||
}
|
||||
|
||||
func (srv *DialService) Close(Round uint32, _ *struct{}) error {
|
||||
log.WithFields(log.Fields{"service": "dial", "rpc": "Close", "round": Round}).Info()
|
||||
|
||||
round, err := srv.getRound(Round, dialRoundOpen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv.filterIncoming(round)
|
||||
|
||||
round.noiseWg.Wait()
|
||||
round.incoming = append(round.incoming, round.noise...)
|
||||
|
||||
shuffler := NewShuffler(rand.Reader, len(round.incoming))
|
||||
shuffler.Shuffle(round.incoming)
|
||||
|
||||
if !srv.LastServer {
|
||||
if err := NewDialRound(srv.Client, Round); err != nil {
|
||||
return fmt.Errorf("NewDialRound: %s", err)
|
||||
}
|
||||
srv.Idle.Unlock()
|
||||
|
||||
if err := RunDialRound(srv.Client, Round, round.incoming); err != nil {
|
||||
return fmt.Errorf("RunDialRound: %s", err)
|
||||
}
|
||||
round.incoming = nil
|
||||
} else {
|
||||
srv.Idle.Unlock()
|
||||
}
|
||||
round.noise = nil
|
||||
|
||||
round.status = dialRoundClosed
|
||||
return nil
|
||||
}
|
||||
|
||||
type DialBucketsArgs struct {
|
||||
Round uint32
|
||||
}
|
||||
|
||||
type DialBucketsResult struct {
|
||||
Buckets [][][SizeEncryptedIntro]byte
|
||||
}
|
||||
|
||||
func (srv *DialService) Buckets(args *DialBucketsArgs, result *DialBucketsResult) error {
|
||||
log.WithFields(log.Fields{"service": "dial", "rpc": "Buckets", "round": args.Round}).Info()
|
||||
|
||||
if !srv.LastServer {
|
||||
return fmt.Errorf("Dial.Buckets can only be called on the last server")
|
||||
}
|
||||
|
||||
round, err := srv.getRound(args.Round, dialRoundClosed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buckets := make([][][SizeEncryptedIntro]byte, TotalDialBuckets)
|
||||
|
||||
ex := new(DialExchange)
|
||||
for _, m := range round.incoming {
|
||||
if len(m) != SizeDialExchange {
|
||||
continue
|
||||
}
|
||||
if err := ex.Unmarshal(m); err != nil {
|
||||
continue
|
||||
}
|
||||
if ex.Bucket >= uint32(len(buckets)) {
|
||||
continue
|
||||
}
|
||||
buckets[ex.Bucket] = append(buckets[ex.Bucket], ex.EncryptedIntro)
|
||||
}
|
||||
|
||||
result.Buckets = buckets
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO we should probably have a corresponding Delete rpc
|
||||
|
||||
func NewDialRound(client *vrpc.Client, round uint32) error {
|
||||
return client.Call("DialService.NewRound", round, nil)
|
||||
}
|
||||
|
||||
func RunDialRound(client *vrpc.Client, round uint32, onions [][]byte) error {
|
||||
spans := Spans(len(onions), 4000)
|
||||
calls := make([]*vrpc.Call, len(spans))
|
||||
|
||||
ParallelFor(len(calls), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
span := spans[i]
|
||||
calls[i] = &vrpc.Call{
|
||||
Method: "DialService.Add",
|
||||
Args: &DialAddArgs{
|
||||
Round: round,
|
||||
Onions: onions[span.Start : span.Start+span.Count],
|
||||
},
|
||||
Reply: nil,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := client.CallMany(calls); err != nil {
|
||||
return fmt.Errorf("Add: %s", err)
|
||||
}
|
||||
|
||||
if err := client.Call("DialService.Close", round, nil); err != nil {
|
||||
return fmt.Errorf("Close: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
// Types used by the entry server and client
|
||||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=MsgType
|
||||
type MsgType uint8
|
||||
|
||||
const (
|
||||
// from client to server
|
||||
MsgConvoRequest MsgType = iota
|
||||
MsgDialRequest
|
||||
|
||||
// from server to client
|
||||
MsgBadRequestError
|
||||
MsgConvoError
|
||||
MsgConvoResponse
|
||||
MsgDialError
|
||||
MsgDialBucket
|
||||
MsgAnnounceConvoRound
|
||||
MsgAnnounceDialRound
|
||||
)
|
||||
|
||||
type Envelope struct {
|
||||
Type MsgType
|
||||
Message json.RawMessage
|
||||
}
|
||||
|
||||
func (e *Envelope) Open() (interface{}, error) {
|
||||
var v interface{}
|
||||
switch e.Type {
|
||||
case MsgConvoRequest:
|
||||
v = new(ConvoRequest)
|
||||
case MsgDialRequest:
|
||||
v = new(DialRequest)
|
||||
case MsgBadRequestError:
|
||||
v = new(BadRequestError)
|
||||
case MsgConvoError:
|
||||
v = new(ConvoError)
|
||||
case MsgConvoResponse:
|
||||
v = new(ConvoResponse)
|
||||
case MsgDialBucket:
|
||||
v = new(DialBucket)
|
||||
case MsgAnnounceConvoRound:
|
||||
v = new(AnnounceConvoRound)
|
||||
case MsgAnnounceDialRound:
|
||||
v = new(AnnounceDialRound)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type: %d", e.Type)
|
||||
}
|
||||
if err := json.Unmarshal(e.Message, v); err != nil {
|
||||
return nil, fmt.Errorf("json.Unmarshal: %s", err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func Envelop(v interface{}) (*Envelope, error) {
|
||||
var t MsgType
|
||||
switch v.(type) {
|
||||
case *ConvoRequest:
|
||||
t = MsgConvoRequest
|
||||
case *DialRequest:
|
||||
t = MsgDialRequest
|
||||
case *BadRequestError:
|
||||
t = MsgBadRequestError
|
||||
case *ConvoError:
|
||||
t = MsgConvoError
|
||||
case *ConvoResponse:
|
||||
t = MsgConvoResponse
|
||||
case *DialError:
|
||||
t = MsgDialError
|
||||
case *DialBucket:
|
||||
t = MsgDialBucket
|
||||
case *AnnounceConvoRound:
|
||||
t = MsgAnnounceConvoRound
|
||||
case *AnnounceDialRound:
|
||||
t = MsgAnnounceDialRound
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported message type: %T", v)
|
||||
}
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json.Marshal: %s", err)
|
||||
}
|
||||
return &Envelope{
|
||||
Type: t,
|
||||
Message: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ConvoRequest struct {
|
||||
Round uint32
|
||||
Onion []byte
|
||||
}
|
||||
|
||||
type DialRequest struct {
|
||||
Round uint32
|
||||
Onion []byte
|
||||
}
|
||||
|
||||
type BadRequestError struct {
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e *BadRequestError) Error() string {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ConvoError struct {
|
||||
Round uint32
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e *ConvoError) Error() string {
|
||||
return fmt.Sprintf("round c%d: %s", e.Round, e.Err)
|
||||
}
|
||||
|
||||
type ConvoResponse struct {
|
||||
Round uint32
|
||||
Onion []byte
|
||||
}
|
||||
|
||||
type DialError struct {
|
||||
Round uint32
|
||||
Err string
|
||||
}
|
||||
|
||||
func (e *DialError) Error() string {
|
||||
return fmt.Sprintf("round d%d: %s", e.Round, e.Err)
|
||||
}
|
||||
|
||||
type DialBucket struct {
|
||||
Round uint32
|
||||
Intros [][SizeEncryptedIntro]byte
|
||||
}
|
||||
|
||||
type AnnounceConvoRound struct {
|
||||
Round uint32
|
||||
}
|
||||
|
||||
type AnnounceDialRound struct {
|
||||
Round uint32
|
||||
Buckets uint32
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ansiCode int
|
||||
|
||||
const (
|
||||
ansiReset ansiCode = 0
|
||||
ansiReverse = 7
|
||||
red = 31
|
||||
green = 32
|
||||
yellow = 33
|
||||
blue = 34
|
||||
magenta = 35
|
||||
cyan = 36
|
||||
)
|
||||
|
||||
var allColors = []ansiCode{red, green, yellow, blue, magenta, cyan}
|
||||
|
||||
type ansiFormatter struct {
|
||||
value interface{}
|
||||
codes []ansiCode
|
||||
}
|
||||
|
||||
func color(value interface{}, codes ...ansiCode) interface{} {
|
||||
if len(codes) == 0 {
|
||||
return value
|
||||
}
|
||||
return &ansiFormatter{value, codes}
|
||||
}
|
||||
|
||||
func (af *ansiFormatter) Format(f fmt.State, c rune) {
|
||||
// reconstruct the format string in bf
|
||||
bf := new(bytes.Buffer)
|
||||
bf.WriteByte('%')
|
||||
for _, x := range []byte{'-', '+', '#', ' ', '0'} {
|
||||
if f.Flag(int(x)) {
|
||||
bf.WriteByte(x)
|
||||
}
|
||||
}
|
||||
if w, ok := f.Width(); ok {
|
||||
fmt.Fprint(bf, w)
|
||||
}
|
||||
if p, ok := f.Precision(); ok {
|
||||
fmt.Fprintf(bf, ".%d", p)
|
||||
}
|
||||
bf.WriteRune(c)
|
||||
format := bf.String()
|
||||
|
||||
if len(af.codes) == 0 {
|
||||
fmt.Fprintf(f, format, af.value)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(f, "\x1b[%d", af.codes[0])
|
||||
for _, code := range af.codes[1:] {
|
||||
fmt.Fprintf(f, ";%d", code)
|
||||
}
|
||||
f.Write([]byte{'m'})
|
||||
fmt.Fprintf(f, format, af.value)
|
||||
fmt.Fprint(f, "\x1b[0m")
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package box
|
|
@ -0,0 +1,31 @@
|
|||
package box
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkSeal(b *testing.B) {
|
||||
_, myPrivate, _ := box.GenerateKey(rand.Reader)
|
||||
theirPublic, _, _ := box.GenerateKey(rand.Reader)
|
||||
message := make([]byte, 256)
|
||||
nonce := new([24]byte)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
box.Seal(nil, message, nonce, theirPublic, myPrivate)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSealAfterPrecomputation(b *testing.B) {
|
||||
_, myPrivate, _ := box.GenerateKey(rand.Reader)
|
||||
theirPublic, _, _ := box.GenerateKey(rand.Reader)
|
||||
message := make([]byte, 256)
|
||||
nonce := new([24]byte)
|
||||
sharedKey := new([32]byte)
|
||||
box.Precompute(sharedKey, theirPublic, myPrivate)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
box.SealAfterPrecomputation(nil, message, nonce, sharedKey)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
func ReadJSONFile(path string, val interface{}) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := json.NewDecoder(f).Decode(val); err != nil {
|
||||
log.Fatalf("json decoding error: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ServerFormatter struct{}
|
||||
|
||||
func (f *ServerFormatter) Format(entry *log.Entry) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
ts := entry.Time.Format("15:04:05")
|
||||
|
||||
var tsColor []ansiCode
|
||||
switch entry.Level {
|
||||
case log.ErrorLevel, log.FatalLevel, log.PanicLevel:
|
||||
tsColor = append(tsColor, red)
|
||||
}
|
||||
if bug, hasBug := entry.Data["bug"].(bool); bug && hasBug {
|
||||
tsColor = append(tsColor, ansiReverse)
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "%s | ", color(ts, tsColor...))
|
||||
|
||||
service, _ := entry.Data["service"].(string)
|
||||
round, hasRound := entry.Data["round"].(uint32)
|
||||
rpc, hasRpc := entry.Data["rpc"].(string)
|
||||
call, hasCall := entry.Data["call"].(string)
|
||||
|
||||
if hasRound {
|
||||
c := allColors[int(round)%len(allColors)]
|
||||
switch service {
|
||||
case "dial":
|
||||
fmt.Fprintf(buf, "%d ", color(round, c, ansiReverse))
|
||||
default:
|
||||
fmt.Fprintf(buf, "%d ", color(round, c))
|
||||
}
|
||||
}
|
||||
|
||||
if hasRpc {
|
||||
fmt.Fprintf(buf, "%s ", rpc)
|
||||
}
|
||||
if hasCall {
|
||||
fmt.Fprintf(buf, "%s ", call)
|
||||
}
|
||||
if entry.Message != "" {
|
||||
fmt.Fprintf(buf, "%s ", entry.Message)
|
||||
}
|
||||
|
||||
for _, k := range []string{"service", "round", "rpc", "call"} {
|
||||
delete(entry.Data, k)
|
||||
}
|
||||
|
||||
if len(entry.Data) > 0 {
|
||||
fmt.Fprint(buf, "| ")
|
||||
writeMap(buf, entry.Data)
|
||||
}
|
||||
|
||||
buf.WriteByte('\n')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type GuiFormatter struct{}
|
||||
|
||||
// TODO gocui doesn't support color text:
|
||||
// https://github.com/jroimartin/gocui/issues/9
|
||||
func (f *GuiFormatter) Format(entry *log.Entry) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
ts := entry.Time.Format("15:04:05")
|
||||
|
||||
fmt.Fprintf(buf, "%s %s | ", ts, entry.Level.String())
|
||||
|
||||
call, hasCall := entry.Data["call"].(string)
|
||||
if hasCall {
|
||||
fmt.Fprintf(buf, "%s ", call)
|
||||
}
|
||||
if entry.Message != "" {
|
||||
fmt.Fprintf(buf, "%s ", entry.Message)
|
||||
}
|
||||
|
||||
for _, k := range []string{"call"} {
|
||||
delete(entry.Data, k)
|
||||
}
|
||||
|
||||
if len(entry.Data) > 0 {
|
||||
fmt.Fprint(buf, "| ")
|
||||
writeMap(buf, entry.Data)
|
||||
}
|
||||
|
||||
buf.WriteByte('\n')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeMap(buf *bytes.Buffer, m map[string]interface{}) {
|
||||
for k, v := range m {
|
||||
buf.WriteString(k)
|
||||
buf.WriteByte('=')
|
||||
switch v := v.(type) {
|
||||
case string, error:
|
||||
fmt.Fprintf(buf, "%q", v)
|
||||
default:
|
||||
fmt.Fprint(buf, v)
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
// parallelfor.go by Jelle van den Hooff
|
||||
package internal
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type GP struct {
|
||||
max, current, step int64
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (gp *GP) Next() (int64, bool) {
|
||||
base := atomic.AddInt64(&gp.current, gp.step) - gp.step
|
||||
|
||||
if base >= gp.max {
|
||||
return 0, false
|
||||
} else {
|
||||
return base, true
|
||||
}
|
||||
}
|
||||
|
||||
type P struct {
|
||||
gp *GP
|
||||
max, current int64
|
||||
}
|
||||
|
||||
func (p *P) Next() (int, bool) {
|
||||
if p.current >= p.max {
|
||||
r, ok := p.gp.Next()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
p.current, p.max = r, r+p.gp.step
|
||||
if p.max > p.gp.max {
|
||||
p.max = p.gp.max
|
||||
}
|
||||
}
|
||||
|
||||
r := p.current
|
||||
p.current += 1
|
||||
|
||||
return int(r), true
|
||||
}
|
||||
|
||||
func ParallelFor(n int, f func(p *P)) {
|
||||
// TODO: this formula could probably be more clever
|
||||
step := n / runtime.NumCPU() / 100
|
||||
if step < 10 {
|
||||
step = 10
|
||||
}
|
||||
|
||||
gp := &GP{
|
||||
max: int64(n),
|
||||
current: 0,
|
||||
step: int64(step),
|
||||
}
|
||||
|
||||
gp.wg.Add(runtime.NumCPU())
|
||||
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
go func() {
|
||||
p := &P{
|
||||
gp: gp,
|
||||
}
|
||||
f(p)
|
||||
gp.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
gp.wg.Wait()
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package internal
|
||||
|
||||
type Span struct {
|
||||
Start int
|
||||
Count int
|
||||
}
|
||||
|
||||
func Spans(total, spanSize int) []Span {
|
||||
spans := make([]Span, 0, (total+spanSize-1)/spanSize)
|
||||
var c int
|
||||
for i := 0; i < total; i += c {
|
||||
if i+spanSize <= total {
|
||||
c = spanSize
|
||||
} else {
|
||||
c = total - i
|
||||
}
|
||||
spans = append(spans, Span{Start: i, Count: c})
|
||||
}
|
||||
return spans
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSpans(t *testing.T) {
|
||||
testEqual(t, Spans(0, 4000), nil)
|
||||
testEqual(t, Spans(1, 4000), []Span{{0, 1}})
|
||||
testEqual(t, Spans(4000, 4000), []Span{{0, 4000}})
|
||||
testEqual(t, Spans(4001, 4000), []Span{{0, 4000}, {4000, 1}})
|
||||
testEqual(t, Spans(8000, 4000), []Span{{0, 4000}, {4000, 4000}})
|
||||
}
|
||||
|
||||
func testEqual(t *testing.T, actual, expected []Span) {
|
||||
if !equal(actual, expected) {
|
||||
t.Fatalf("expecting %v, got %v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func equal(a, b []Span) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
)
|
||||
|
||||
func laplace(mu, b float64) float64 {
|
||||
var r [8]byte
|
||||
if _, err := rand.Read(r[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
x := binary.BigEndian.Uint64(r[:])
|
||||
u := float64(x)/float64(^uint64(0)) - .5
|
||||
|
||||
var abs, sign float64
|
||||
if u < 0 {
|
||||
abs = -u
|
||||
sign = -1
|
||||
} else {
|
||||
abs = u
|
||||
sign = 1
|
||||
}
|
||||
|
||||
return mu - b*sign*math.Log(1-2*abs)
|
||||
}
|
||||
|
||||
func cappedFlooredLaplace(mu, b float64) int {
|
||||
x := laplace(mu, b)
|
||||
if x < 0 {
|
||||
return cappedFlooredLaplace(mu, b)
|
||||
}
|
||||
|
||||
return int(x)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// generated by stringer -type=MsgType; DO NOT EDIT
|
||||
|
||||
package vuvuzela
|
||||
|
||||
import "fmt"
|
||||
|
||||
const _MsgType_name = "MsgConvoRequestMsgDialRequestMsgBadRequestErrorMsgConvoErrorMsgConvoResponseMsgDialErrorMsgDialBucketMsgAnnounceConvoRoundMsgAnnounceDialRound"
|
||||
|
||||
var _MsgType_index = [...]uint8{0, 15, 29, 47, 60, 76, 88, 101, 122, 142}
|
||||
|
||||
func (i MsgType) String() string {
|
||||
if i >= MsgType(len(_MsgType_index)-1) {
|
||||
return fmt.Sprintf("MsgType(%d)", i)
|
||||
}
|
||||
return _MsgType_name[_MsgType_index[i]:_MsgType_index[i+1]]
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/onionbox"
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
)
|
||||
|
||||
func FillWithFakeSingles(dest [][]byte, nonce *[24]byte, nextKeys []*[32]byte) {
|
||||
ParallelFor(len(dest), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
var exchange [SizeConvoExchange]byte
|
||||
rand.Read(exchange[:])
|
||||
onion, _ := onionbox.Seal(exchange[:], nonce, nextKeys)
|
||||
dest[i] = onion
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FillWithFakeDoubles(dest [][]byte, nonce *[24]byte, nextKeys []*[32]byte) {
|
||||
ParallelFor(len(dest)/2, func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
var exchange1 [SizeConvoExchange]byte
|
||||
var exchange2 [SizeConvoExchange]byte
|
||||
rand.Read(exchange1[:])
|
||||
copy(exchange2[0:16], exchange1[0:16])
|
||||
rand.Read(exchange2[16:])
|
||||
onion1, _ := onionbox.Seal(exchange1[:], nonce, nextKeys)
|
||||
onion2, _ := onionbox.Seal(exchange2[:], nonce, nextKeys)
|
||||
dest[i*2] = onion1
|
||||
dest[i*2+1] = onion2
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FillWithFakeIntroductions(dest [][]byte, noiseCounts []int, nonce *[24]byte, nextKeys []*[32]byte) {
|
||||
buckets := make([]int, len(dest))
|
||||
idx := 0
|
||||
for b, count := range noiseCounts {
|
||||
for i := 0; i < count; i++ {
|
||||
buckets[idx] = b
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
ParallelFor(len(dest), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
var exchange [SizeDialExchange]byte
|
||||
binary.BigEndian.PutUint32(exchange[0:4], uint32(buckets[i]))
|
||||
rand.Read(exchange[4:])
|
||||
onion, _ := onionbox.Seal(exchange[:], nonce, nextKeys)
|
||||
dest[i] = onion
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
var mu = 100000
|
||||
|
||||
const numKeys = 2
|
||||
|
||||
func BenchmarkFillWithFakeSingles(b *testing.B) {
|
||||
noise := make([][]byte, mu)
|
||||
keys := genKeys(numKeys)
|
||||
nonce := new([24]byte)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
FillWithFakeSingles(noise, nonce, keys)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFillWithFakeDoubles(b *testing.B) {
|
||||
noise := make([][]byte, mu)
|
||||
keys := genKeys(numKeys)
|
||||
nonce := new([24]byte)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
FillWithFakeDoubles(noise, nonce, keys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
flag.IntVar(&mu, "mu", 100000, "mu value")
|
||||
flag.Parse()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func genKeys(i int) []*[32]byte {
|
||||
keys := make([]*[32]byte, i)
|
||||
for i := range keys {
|
||||
pub, _, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keys[i] = pub
|
||||
}
|
||||
return keys
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package onionbox
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
)
|
||||
|
||||
// Overhead of one layer
|
||||
const Overhead = 32 + box.Overhead
|
||||
|
||||
func Seal(message []byte, nonce *[24]byte, publicKeys []*[32]byte) ([]byte, []*[32]byte) {
|
||||
onion := message
|
||||
sharedKeys := make([]*[32]byte, len(publicKeys))
|
||||
for i := len(publicKeys) - 1; i >= 0; i-- {
|
||||
myPublicKey, myPrivateKey, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sharedKeys[i] = new([32]byte)
|
||||
box.Precompute(sharedKeys[i], (*[32]byte)(publicKeys[i]), myPrivateKey)
|
||||
|
||||
onion = box.SealAfterPrecomputation(myPublicKey[:], onion, nonce, sharedKeys[i])
|
||||
}
|
||||
|
||||
return onion, sharedKeys
|
||||
}
|
||||
|
||||
func Open(onion []byte, nonce *[24]byte, sharedKeys []*[32]byte) ([]byte, bool) {
|
||||
var ok bool
|
||||
message := onion
|
||||
for i := 0; i < len(sharedKeys); i++ {
|
||||
message, ok = box.OpenAfterPrecomputation(nil, message, nonce, sharedKeys[i])
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
return message, true
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SizeMessage = 240
|
||||
|
||||
// Eventually this might be dynamic, but one bucket is usually
|
||||
// sufficient if users don't dial very often.
|
||||
TotalDialBuckets = 1
|
||||
|
||||
DialWait = 10 * time.Second
|
||||
DefaultReceiveWait = 5 * time.Second
|
||||
|
||||
DefaultServerAddr = ":2718"
|
||||
DefaultServerPort = "2718"
|
||||
)
|
|
@ -0,0 +1,106 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/onionbox"
|
||||
)
|
||||
|
||||
type ServerInfo struct {
|
||||
Address string
|
||||
PublicKey *BoxKey
|
||||
}
|
||||
|
||||
type PKI struct {
|
||||
People map[string]*BoxKey
|
||||
Servers map[string]*ServerInfo
|
||||
ServerOrder []string
|
||||
EntryServer string
|
||||
}
|
||||
|
||||
func ReadPKI(jsonPath string) *PKI {
|
||||
pki := new(PKI)
|
||||
ReadJSONFile(jsonPath, pki)
|
||||
if len(pki.ServerOrder) == 0 {
|
||||
log.Fatalf("%q: ServerOrder must contain at least one server", jsonPath)
|
||||
}
|
||||
for _, s := range pki.ServerOrder {
|
||||
info, ok := pki.Servers[s]
|
||||
if !ok {
|
||||
log.Fatalf("%q: server %q not found", jsonPath, s)
|
||||
}
|
||||
addr := info.Address
|
||||
if addr == "" {
|
||||
log.Fatalf("%q: server %q does not specify an Address", jsonPath, s)
|
||||
}
|
||||
|
||||
if strings.IndexByte(addr, ':') == -1 {
|
||||
info.Address = net.JoinHostPort(addr, DefaultServerPort)
|
||||
}
|
||||
}
|
||||
return pki
|
||||
}
|
||||
|
||||
func (pki *PKI) ServerKeys() BoxKeys {
|
||||
keys := make([]*BoxKey, 0, 3)
|
||||
for _, s := range pki.ServerOrder {
|
||||
info := pki.Servers[s]
|
||||
keys = append(keys, info.PublicKey)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (pki *PKI) FirstServer() string {
|
||||
s := pki.ServerOrder[0]
|
||||
return pki.Servers[s].Address
|
||||
}
|
||||
|
||||
func (pki *PKI) LastServer() string {
|
||||
s := pki.ServerOrder[len(pki.ServerOrder)-1]
|
||||
return pki.Servers[s].Address
|
||||
}
|
||||
|
||||
func (pki *PKI) Index(serverName string) int {
|
||||
var i int
|
||||
var s string
|
||||
for i, s = range pki.ServerOrder {
|
||||
if s == serverName {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (pki *PKI) NextServer(serverName string) string {
|
||||
i := pki.Index(serverName)
|
||||
if i < len(pki.ServerOrder)-1 {
|
||||
s := pki.ServerOrder[i+1]
|
||||
return pki.Servers[s].Address
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (pki *PKI) NextServerKeys(serverName string) BoxKeys {
|
||||
i := pki.Index(serverName)
|
||||
var keys []*BoxKey
|
||||
for _, s := range pki.ServerOrder[i+1:] {
|
||||
keys = append(keys, pki.Servers[s].PublicKey)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (pki *PKI) IncomingOnionOverhead(serverName string) int {
|
||||
i := len(pki.ServerOrder) - pki.Index(serverName)
|
||||
return i * onionbox.Overhead
|
||||
}
|
||||
|
||||
func (pki *PKI) OutgoingOnionOverhead(serverName string) int {
|
||||
i := len(pki.ServerOrder) - pki.Index(serverName)
|
||||
return i * box.Overhead
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Key(s string) *BoxKey {
|
||||
k, err := KeyFromString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
var testPKI = &PKI{
|
||||
People: map[string]*BoxKey{
|
||||
"david": Key("st50pjmxgzv6pybrnxrxjd330s8hf37g5gzs1dqywy4bw3kdvcgg"),
|
||||
"alice": Key("j10hpqtgnqc1y21xp5y7yamwa32jvdp89888q2semnxg95j4v82g"),
|
||||
},
|
||||
Servers: map[string]*ServerInfo{
|
||||
"openstack1": {
|
||||
Address: "localhost",
|
||||
PublicKey: Key("pd04y1ryrfxtrayjg9f4cfsw1ayfhwrcfd7g7emhfjrsc4cd20f0"),
|
||||
},
|
||||
"openstack2": {
|
||||
Address: "localhost:2719",
|
||||
PublicKey: Key("fkaf8ds0a4fmdsztqzpcn4em9npyv722bxv2683n9fdydzdjwgy0"),
|
||||
},
|
||||
},
|
||||
ServerOrder: []string{"openstack1", "openstack2"},
|
||||
}
|
||||
|
||||
func TestNextKeys(t *testing.T) {
|
||||
nextKeys := testPKI.NextServerKeys("openstack1")
|
||||
if len(nextKeys) != 1 {
|
||||
t.Fatalf("wrong length")
|
||||
}
|
||||
if nextKeys[0] != testPKI.Servers["openstack2"].PublicKey {
|
||||
t.Fatalf("wrong key")
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
`cpu*` code from https://github.com/jonhoo/drwmutex
|
|
@ -0,0 +1,11 @@
|
|||
// +build !amd64
|
||||
|
||||
package rand
|
||||
|
||||
// cpu returns a unique identifier for the core the current goroutine is
|
||||
// executing on. This function is platform dependent, and is implemented in
|
||||
// cpu_*.s.
|
||||
func cpu() uint64 {
|
||||
// this reverts the behaviour to that of a regular DRWMutex
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package rand
|
||||
|
||||
func cpu() uint64
|
|
@ -0,0 +1,15 @@
|
|||
#include "textflag.h"
|
||||
|
||||
// func cpu() uint64
|
||||
TEXT ·cpu(SB),NOSPLIT,$0-8
|
||||
MOVL $0x01, AX // version information
|
||||
MOVL $0x00, BX // any leaf will do
|
||||
MOVL $0x00, CX // any subleaf will do
|
||||
|
||||
// call CPUID
|
||||
BYTE $0x0f
|
||||
BYTE $0xa2
|
||||
|
||||
SHRQ $24, BX // logical cpu id is put in EBX[31-24]
|
||||
MOVQ BX, ret+0(FP)
|
||||
RET
|
|
@ -0,0 +1,107 @@
|
|||
package rand
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/salsa20"
|
||||
)
|
||||
|
||||
// TODO we should occasionally reseed the PRNG
|
||||
|
||||
type Salsa20Rand struct {
|
||||
zeroes []byte
|
||||
buffer []byte
|
||||
bufferOffset int
|
||||
|
||||
key [32]byte
|
||||
nonce uint64
|
||||
}
|
||||
|
||||
func NewSalsa20Rand(base io.Reader) *Salsa20Rand {
|
||||
n := 16 * 1024
|
||||
|
||||
sr := &Salsa20Rand{
|
||||
zeroes: make([]byte, n),
|
||||
buffer: make([]byte, n),
|
||||
bufferOffset: n,
|
||||
nonce: 0,
|
||||
}
|
||||
if n, err := base.Read(sr.key[:]); n != 32 || err != nil {
|
||||
panic("NewSalsa20Rand: " + err.Error())
|
||||
}
|
||||
sr.fill()
|
||||
return sr
|
||||
}
|
||||
|
||||
func (sr *Salsa20Rand) Read(d []byte) (n int, err error) {
|
||||
for len(d) > 0 {
|
||||
if sr.bufferOffset == len(sr.buffer) {
|
||||
sr.fill()
|
||||
}
|
||||
|
||||
m := copy(d, sr.buffer[sr.bufferOffset:])
|
||||
d = d[m:]
|
||||
sr.bufferOffset += m
|
||||
n += m
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sr *Salsa20Rand) fill() {
|
||||
var nonce [8]byte
|
||||
binary.BigEndian.PutUint64(nonce[:], sr.nonce)
|
||||
sr.nonce += 1
|
||||
|
||||
salsa20.XORKeyStream(sr.buffer, sr.zeroes, nonce[:], &sr.key)
|
||||
sr.bufferOffset = 0
|
||||
}
|
||||
|
||||
type MutexReader struct {
|
||||
mu sync.Mutex
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func NewMutexReader(reader io.Reader) io.Reader {
|
||||
return &MutexReader{
|
||||
reader: reader,
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *MutexReader) Read(d []byte) (n int, err error) {
|
||||
mr.mu.Lock()
|
||||
n, err = mr.reader.Read(d)
|
||||
mr.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
type PerCPUReader struct {
|
||||
readers []io.Reader
|
||||
}
|
||||
|
||||
func NewPerCPUReader(initfunc func() io.Reader) io.Reader {
|
||||
readers := make([]io.Reader, runtime.NumCPU())
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
readers[i] = initfunc()
|
||||
}
|
||||
return &PerCPUReader{
|
||||
readers: readers,
|
||||
}
|
||||
}
|
||||
|
||||
func (pcr *PerCPUReader) Read(d []byte) (n int, err error) {
|
||||
thiscpu := cpu()
|
||||
return pcr.readers[thiscpu%uint64(runtime.NumCPU())].Read(d)
|
||||
}
|
||||
|
||||
var Reader = NewPerCPUReader(func() io.Reader {
|
||||
return NewMutexReader(NewSalsa20Rand(rand.Reader))
|
||||
})
|
||||
|
||||
func Read(b []byte) (n int, err error) {
|
||||
return io.ReadFull(Reader, b)
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package rand
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type zeroReader struct{}
|
||||
|
||||
func (zr *zeroReader) Read(d []byte) (n int, err error) {
|
||||
for i := range d {
|
||||
d[i] = 0
|
||||
}
|
||||
return len(d), nil
|
||||
}
|
||||
|
||||
func TestRandomReads(t *testing.T) {
|
||||
rest := 1024 * 1024
|
||||
hash := sha256.New()
|
||||
srand := NewSalsa20Rand(new(zeroReader))
|
||||
|
||||
mrand.Seed(time.Now().Unix())
|
||||
for rest > 0 {
|
||||
n := mrand.Intn(1024)
|
||||
if n > rest {
|
||||
n = rest
|
||||
}
|
||||
|
||||
d := make([]byte, n)
|
||||
if _, err := srand.Read(d); err != nil {
|
||||
t.Fatalf("error: %s", err)
|
||||
}
|
||||
|
||||
hash.Write(d)
|
||||
rest -= n
|
||||
}
|
||||
|
||||
sum := hash.Sum(nil)
|
||||
expected := "12ccf37d07f2a467350971bbb7e83fe198f96bdd94b302ac52b100f330a466d8"
|
||||
actually := fmt.Sprintf("%x", sum)
|
||||
if actually != expected {
|
||||
t.Fatalf("\nexpected: %s\nactually: %s", expected, actually)
|
||||
}
|
||||
}
|
||||
|
||||
const total = 100 * 1024 * 1024
|
||||
|
||||
func BenchmarkCryptoRand(b *testing.B) {
|
||||
x := make([]byte, total)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := rand.Read(x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBufioRand(b *testing.B) {
|
||||
x := make([]byte, total)
|
||||
buf := bufio.NewReader(rand.Reader)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := buf.Read(x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDevUrandom(b *testing.B) {
|
||||
x := make([]byte, total)
|
||||
f, err := os.Open("/dev/urandom")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buf := bufio.NewReader(f)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := buf.Read(x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVuvuzelaRand(b *testing.B) {
|
||||
x := make([]byte, total)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := Read(x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFasterRand(b *testing.B) {
|
||||
x := make([]byte, total)
|
||||
b.ResetTimer()
|
||||
fr := NewSalsa20Rand(Reader)
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := fr.Read(x); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
After Width: | Height: | Size: 616 KiB |
|
@ -0,0 +1,45 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Shuffler []int
|
||||
|
||||
func NewShuffler(rand io.Reader, n int) Shuffler {
|
||||
p := make(Shuffler, n)
|
||||
for i := range p {
|
||||
p[i] = Intn(rand, i+1)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s Shuffler) Shuffle(x [][]byte) {
|
||||
for i := range x {
|
||||
j := s[i]
|
||||
x[i], x[j] = x[j], x[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (s Shuffler) Unshuffle(x [][]byte) {
|
||||
for i := len(x) - 1; i >= 0; i-- {
|
||||
j := s[i]
|
||||
x[i], x[j] = x[j], x[i]
|
||||
}
|
||||
}
|
||||
|
||||
func Intn(rand io.Reader, n int) int {
|
||||
max := ^uint32(0)
|
||||
m := max % uint32(n)
|
||||
r := make([]byte, 4)
|
||||
for {
|
||||
if _, err := rand.Read(r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
x := binary.BigEndian.Uint32(r)
|
||||
if x < max-m {
|
||||
return int(x % uint32(n))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davidlazar/vuvuzela/rand"
|
||||
)
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
n := 64
|
||||
x := make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
x[i] = []byte{byte(i)}
|
||||
}
|
||||
|
||||
s := NewShuffler(rand.Reader, len(x))
|
||||
s.Shuffle(x)
|
||||
|
||||
allSame := true
|
||||
for i := 0; i < n; i++ {
|
||||
if x[i][0] != byte(i) {
|
||||
allSame = false
|
||||
}
|
||||
}
|
||||
|
||||
if allSame {
|
||||
t.Errorf("shuffler isn't shuffling")
|
||||
}
|
||||
|
||||
s.Unshuffle(x)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if x[i][0] != byte(i) {
|
||||
t.Errorf("unshuffle does not undo shuffle")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package vrpc
|
||||
|
||||
import (
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
rpcClients []*rpc.Client
|
||||
}
|
||||
|
||||
func Dial(network, address string, connections int) (*Client, error) {
|
||||
rpcClients := make([]*rpc.Client, connections)
|
||||
for i := range rpcClients {
|
||||
c, err := rpc.Dial(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rpcClients[i] = c
|
||||
}
|
||||
return &Client{
|
||||
rpcClients: rpcClients,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Call(method string, args interface{}, reply interface{}) error {
|
||||
return c.rpcClients[0].Call(method, args, reply)
|
||||
}
|
||||
|
||||
type Call struct {
|
||||
Method string
|
||||
Args interface{}
|
||||
Reply interface{}
|
||||
}
|
||||
|
||||
func (c *Client) CallMany(calls []*Call) error {
|
||||
if len(calls) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
callChan := make(chan *Call, 4)
|
||||
|
||||
go func() {
|
||||
for _, c := range calls {
|
||||
select {
|
||||
case callChan <- c:
|
||||
// ok
|
||||
case <-done:
|
||||
break
|
||||
}
|
||||
}
|
||||
close(callChan)
|
||||
}()
|
||||
|
||||
results := make(chan *rpc.Call, len(calls))
|
||||
for _, rc := range c.rpcClients {
|
||||
go func(rc *rpc.Client) {
|
||||
for call := range callChan {
|
||||
rc.Go(call.Method, call.Args, call.Reply, results)
|
||||
}
|
||||
}(rc)
|
||||
}
|
||||
|
||||
var err error
|
||||
var received int
|
||||
for call := range results {
|
||||
err = call.Error
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
received++
|
||||
if received == len(calls) {
|
||||
close(results)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
close(done)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
sync.Mutex
|
||||
|
||||
EntryServer string
|
||||
MyPublicKey *BoxKey
|
||||
|
||||
ws *websocket.Conn
|
||||
|
||||
roundHandlers map[uint32]ConvoHandler
|
||||
convoHandler ConvoHandler
|
||||
dialHandler DialHandler
|
||||
}
|
||||
|
||||
type ConvoHandler interface {
|
||||
NextConvoRequest(round uint32) *ConvoRequest
|
||||
HandleConvoResponse(response *ConvoResponse)
|
||||
}
|
||||
|
||||
type DialHandler interface {
|
||||
NextDialRequest(round uint32, buckets uint32) *DialRequest
|
||||
HandleDialBucket(db *DialBucket)
|
||||
}
|
||||
|
||||
func NewClient(entryServer string, publicKey *BoxKey) *Client {
|
||||
c := &Client{
|
||||
EntryServer: entryServer,
|
||||
MyPublicKey: publicKey,
|
||||
|
||||
roundHandlers: make(map[uint32]ConvoHandler),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) SetConvoHandler(convo ConvoHandler) {
|
||||
c.Lock()
|
||||
c.convoHandler = convo
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) SetDialHandler(dialer DialHandler) {
|
||||
c.Lock()
|
||||
c.dialHandler = dialer
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) Connect() error {
|
||||
// TODO check if already connected
|
||||
if c.convoHandler == nil {
|
||||
return fmt.Errorf("no convo handler")
|
||||
}
|
||||
if c.dialHandler == nil {
|
||||
return fmt.Errorf("no dial handler")
|
||||
}
|
||||
|
||||
wsaddr := fmt.Sprintf("%s/ws?publickey=%s", c.EntryServer, c.MyPublicKey.String())
|
||||
dialer := &websocket.Dialer{
|
||||
HandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
ws, _, err := dialer.Dial(wsaddr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.ws = ws
|
||||
go c.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
c.ws.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Send(v interface{}) {
|
||||
const writeWait = 10 * time.Second
|
||||
|
||||
e, err := Envelop(v)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"bug": true, "call": "Envelop"}).Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := c.ws.WriteJSON(e); err != nil {
|
||||
log.WithFields(log.Fields{"call": "WriteJSON"}).Debug(err)
|
||||
c.Unlock()
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) readLoop() {
|
||||
for {
|
||||
var e Envelope
|
||||
if err := c.ws.ReadJSON(&e); err != nil {
|
||||
log.WithFields(log.Fields{"call": "ReadJSON"}).Debug(err)
|
||||
c.Close()
|
||||
break
|
||||
}
|
||||
|
||||
v, err := e.Open()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "Envelope.Open"}).Error(err)
|
||||
continue
|
||||
}
|
||||
go c.handleResponse(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleResponse(v interface{}) {
|
||||
switch v := v.(type) {
|
||||
case *BadRequestError:
|
||||
log.Printf("bad request error: %s", v.Error())
|
||||
case *AnnounceConvoRound:
|
||||
c.Send(c.nextConvoRequest(v.Round))
|
||||
case *AnnounceDialRound:
|
||||
c.Send(c.dialHandler.NextDialRequest(v.Round, v.Buckets))
|
||||
case *ConvoResponse:
|
||||
c.deliverConvoResponse(v)
|
||||
case *DialBucket:
|
||||
c.dialHandler.HandleDialBucket(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) nextConvoRequest(round uint32) *ConvoRequest {
|
||||
c.Lock()
|
||||
c.roundHandlers[round] = c.convoHandler
|
||||
c.Unlock()
|
||||
return c.convoHandler.NextConvoRequest(round)
|
||||
}
|
||||
|
||||
func (c *Client) deliverConvoResponse(r *ConvoResponse) {
|
||||
c.Lock()
|
||||
convo, ok := c.roundHandlers[r.Round]
|
||||
delete(c.roundHandlers, r.Round)
|
||||
c.Unlock()
|
||||
if !ok {
|
||||
log.WithFields(log.Fields{"round": r.Round}).Error("round not found")
|
||||
return
|
||||
}
|
||||
|
||||
convo.HandleConvoResponse(r)
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
"github.com/davidlazar/vuvuzela/onionbox"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
sync.RWMutex
|
||||
|
||||
pki *PKI
|
||||
peerName string
|
||||
peerPublicKey *BoxKey
|
||||
myPublicKey *BoxKey
|
||||
myPrivateKey *BoxKey
|
||||
gui *GuiClient
|
||||
|
||||
outQueue chan []byte
|
||||
pendingRounds map[uint32]*pendingRound
|
||||
|
||||
lastPeerResponding bool
|
||||
lastLatency time.Duration
|
||||
lastRound uint32
|
||||
}
|
||||
|
||||
func (c *Conversation) Init() {
|
||||
c.Lock()
|
||||
c.outQueue = make(chan []byte, 64)
|
||||
c.pendingRounds = make(map[uint32]*pendingRound)
|
||||
c.lastPeerResponding = false
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
type pendingRound struct {
|
||||
onionSharedKeys []*[32]byte
|
||||
sentMessage [SizeEncryptedMessage]byte
|
||||
}
|
||||
|
||||
type ConvoMessage struct {
|
||||
Body interface{}
|
||||
// seq/ack numbers can go here
|
||||
}
|
||||
|
||||
type TextMessage struct {
|
||||
Message []byte
|
||||
}
|
||||
|
||||
type TimestampMessage struct {
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (cm *ConvoMessage) Marshal() (msg [SizeMessage]byte) {
|
||||
switch v := cm.Body.(type) {
|
||||
case *TimestampMessage:
|
||||
msg[0] = 0
|
||||
binary.PutVarint(msg[1:], v.Timestamp.Unix())
|
||||
case *TextMessage:
|
||||
msg[0] = 1
|
||||
copy(msg[1:], v.Message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cm *ConvoMessage) Unmarshal(msg []byte) error {
|
||||
switch msg[0] {
|
||||
case 0:
|
||||
ts, _ := binary.Varint(msg[1:])
|
||||
cm.Body = &TimestampMessage{
|
||||
Timestamp: time.Unix(ts, 0),
|
||||
}
|
||||
case 1:
|
||||
cm.Body = &TextMessage{msg[1:]}
|
||||
default:
|
||||
return fmt.Errorf("unexpected message type: %d", msg[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conversation) QueueTextMessage(msg []byte) {
|
||||
c.outQueue <- msg
|
||||
}
|
||||
|
||||
func (c *Conversation) NextConvoRequest(round uint32) *ConvoRequest {
|
||||
c.Lock()
|
||||
c.lastRound = round
|
||||
c.Unlock()
|
||||
go c.gui.Flush()
|
||||
|
||||
var body interface{}
|
||||
|
||||
select {
|
||||
case m := <-c.outQueue:
|
||||
body = &TextMessage{Message: m}
|
||||
default:
|
||||
body = &TimestampMessage{
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
msg := &ConvoMessage{
|
||||
Body: body,
|
||||
}
|
||||
msgdata := msg.Marshal()
|
||||
|
||||
var encmsg [SizeEncryptedMessage]byte
|
||||
ctxt := c.Seal(msgdata[:], round, c.myRole())
|
||||
copy(encmsg[:], ctxt)
|
||||
|
||||
exchange := &ConvoExchange{
|
||||
DeadDrop: c.deadDrop(round),
|
||||
EncryptedMessage: encmsg,
|
||||
}
|
||||
|
||||
onion, sharedKeys := onionbox.Seal(exchange.Marshal(), ForwardNonce(round), c.pki.ServerKeys().Keys())
|
||||
|
||||
pr := &pendingRound{
|
||||
onionSharedKeys: sharedKeys,
|
||||
sentMessage: encmsg,
|
||||
}
|
||||
c.Lock()
|
||||
c.pendingRounds[round] = pr
|
||||
c.Unlock()
|
||||
|
||||
return &ConvoRequest{
|
||||
Round: round,
|
||||
Onion: onion,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conversation) HandleConvoResponse(r *ConvoResponse) {
|
||||
rlog := log.WithFields(log.Fields{"round": r.Round})
|
||||
|
||||
var responding bool
|
||||
defer func() {
|
||||
c.Lock()
|
||||
c.lastPeerResponding = responding
|
||||
c.Unlock()
|
||||
c.gui.Flush()
|
||||
}()
|
||||
|
||||
c.Lock()
|
||||
pr, ok := c.pendingRounds[r.Round]
|
||||
delete(c.pendingRounds, r.Round)
|
||||
c.Unlock()
|
||||
if !ok {
|
||||
rlog.Error("round not found")
|
||||
return
|
||||
}
|
||||
|
||||
encmsg, ok := onionbox.Open(r.Onion, BackwardNonce(r.Round), pr.onionSharedKeys)
|
||||
if !ok {
|
||||
rlog.Error("decrypting onion failed")
|
||||
return
|
||||
}
|
||||
|
||||
if bytes.Compare(encmsg, pr.sentMessage[:]) == 0 && !c.Solo() {
|
||||
return
|
||||
}
|
||||
|
||||
msgdata, ok := c.Open(encmsg, r.Round, c.theirRole())
|
||||
if !ok {
|
||||
rlog.Error("decrypting peer message failed")
|
||||
return
|
||||
}
|
||||
|
||||
msg := new(ConvoMessage)
|
||||
if err := msg.Unmarshal(msgdata); err != nil {
|
||||
rlog.Error("unmarshaling peer message failed")
|
||||
return
|
||||
}
|
||||
|
||||
responding = true
|
||||
|
||||
switch m := msg.Body.(type) {
|
||||
case *TextMessage:
|
||||
s := strings.TrimRight(string(m.Message), "\x00")
|
||||
c.gui.Printf("<%s> %s\n", c.peerName, s)
|
||||
case *TimestampMessage:
|
||||
latency := time.Now().Sub(m.Timestamp)
|
||||
c.Lock()
|
||||
c.lastLatency = latency
|
||||
c.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
PeerResponding bool
|
||||
Round uint32
|
||||
Latency float64
|
||||
}
|
||||
|
||||
func (c *Conversation) Status() *Status {
|
||||
c.RLock()
|
||||
status := &Status{
|
||||
PeerResponding: c.lastPeerResponding,
|
||||
Round: c.lastRound,
|
||||
Latency: float64(c.lastLatency) / float64(time.Second),
|
||||
}
|
||||
c.RUnlock()
|
||||
return status
|
||||
}
|
||||
|
||||
func (c *Conversation) Solo() bool {
|
||||
return bytes.Compare(c.myPublicKey[:], c.peerPublicKey[:]) == 0
|
||||
}
|
||||
|
||||
// Roles ensure that messages to the peer and messages from
|
||||
// the peer have distinct nonces.
|
||||
func (c *Conversation) myRole() byte {
|
||||
if bytes.Compare(c.myPublicKey[:], c.peerPublicKey[:]) < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conversation) theirRole() byte {
|
||||
if bytes.Compare(c.peerPublicKey[:], c.myPublicKey[:]) < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conversation) Seal(message []byte, round uint32, role byte) []byte {
|
||||
var nonce [24]byte
|
||||
binary.BigEndian.PutUint32(nonce[:], round)
|
||||
nonce[23] = role
|
||||
|
||||
ctxt := box.Seal(nil, message, &nonce, c.peerPublicKey.Key(), c.myPrivateKey.Key())
|
||||
return ctxt
|
||||
}
|
||||
|
||||
func (c *Conversation) Open(ctxt []byte, round uint32, role byte) ([]byte, bool) {
|
||||
var nonce [24]byte
|
||||
binary.BigEndian.PutUint32(nonce[:], round)
|
||||
nonce[23] = role
|
||||
|
||||
return box.Open(nil, ctxt, &nonce, c.peerPublicKey.Key(), c.myPrivateKey.Key())
|
||||
}
|
||||
|
||||
func (c *Conversation) deadDrop(round uint32) (id DeadDrop) {
|
||||
if c.Solo() {
|
||||
rand.Read(id[:])
|
||||
} else {
|
||||
var sharedKey [32]byte
|
||||
box.Precompute(&sharedKey, c.peerPublicKey.Key(), c.myPrivateKey.Key())
|
||||
|
||||
h := hmac.New(sha256.New, sharedKey[:])
|
||||
binary.Write(h, binary.BigEndian, round)
|
||||
r := h.Sum(nil)
|
||||
copy(id[:], r)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
)
|
||||
|
||||
func TestSoloConversation(t *testing.T) {
|
||||
public, private, err := GenerateBoxKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
convo := &Conversation{
|
||||
peerPublicKey: public,
|
||||
myPublicKey: public,
|
||||
myPrivateKey: private,
|
||||
}
|
||||
if convo.myRole() != convo.theirRole() {
|
||||
t.Fatalf("expecting roles to match")
|
||||
}
|
||||
|
||||
msg := make([]byte, 256)
|
||||
rand.Read(msg)
|
||||
|
||||
var round uint32 = 42
|
||||
ctxt := convo.Seal(msg, round, convo.myRole())
|
||||
xmsg, ok := convo.Open(ctxt, round, convo.theirRole())
|
||||
if !ok {
|
||||
t.Fatalf("failed to decrypt message")
|
||||
}
|
||||
|
||||
if bytes.Compare(msg, xmsg) != 0 {
|
||||
t.Fatalf("messages don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalConvoMessage(t *testing.T) {
|
||||
now := time.Now()
|
||||
tsm := &TimestampMessage{Timestamp: now}
|
||||
cm := &ConvoMessage{Body: tsm}
|
||||
data := cm.Marshal()
|
||||
|
||||
xcm := new(ConvoMessage)
|
||||
if err := xcm.Unmarshal(data[:]); err != nil {
|
||||
t.Fatalf("Unmarshal error: %s", err)
|
||||
}
|
||||
|
||||
xtsm := xcm.Body.(*TimestampMessage)
|
||||
if xtsm.Timestamp.Unix() != now.Unix() {
|
||||
t.Fatalf("timestamps don't match")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
"github.com/davidlazar/vuvuzela/onionbox"
|
||||
)
|
||||
|
||||
type Dialer struct {
|
||||
gui *GuiClient
|
||||
pki *PKI
|
||||
myPublicKey *BoxKey
|
||||
myPrivateKey *BoxKey
|
||||
|
||||
userDialRequests chan *BoxKey
|
||||
}
|
||||
|
||||
func (d *Dialer) Init() {
|
||||
d.userDialRequests = make(chan *BoxKey, 4)
|
||||
}
|
||||
|
||||
func (d *Dialer) QueueRequest(publicKey *BoxKey) {
|
||||
d.userDialRequests <- publicKey
|
||||
}
|
||||
|
||||
func (d *Dialer) NextDialRequest(round uint32, buckets uint32) *DialRequest {
|
||||
var ex *DialExchange
|
||||
select {
|
||||
case pk := <-d.userDialRequests:
|
||||
intro := (&Introduction{
|
||||
Rendezvous: round + 4,
|
||||
LongTermKey: *d.myPublicKey,
|
||||
}).Marshal()
|
||||
ctxt, _ := onionbox.Seal(intro, ForwardNonce(round), BoxKeys{pk}.Keys())
|
||||
ex = &DialExchange{
|
||||
Bucket: KeyDialBucket(pk, buckets),
|
||||
}
|
||||
copy(ex.EncryptedIntro[:], ctxt)
|
||||
default:
|
||||
ex = &DialExchange{
|
||||
Bucket: ^uint32(0),
|
||||
}
|
||||
rand.Read(ex.EncryptedIntro[:])
|
||||
}
|
||||
|
||||
onion, _ := onionbox.Seal(ex.Marshal(), ForwardNonce(round), d.pki.ServerKeys().Keys())
|
||||
|
||||
return &DialRequest{
|
||||
Round: round,
|
||||
Onion: onion,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) HandleDialBucket(db *DialBucket) {
|
||||
nonce := ForwardNonce(db.Round)
|
||||
|
||||
OUTER:
|
||||
for _, b := range db.Intros {
|
||||
var pk [32]byte
|
||||
copy(pk[:], b[0:32])
|
||||
data, ok := box.Open(nil, b[32:], nonce, &pk, d.myPrivateKey.Key())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
intro := new(Introduction)
|
||||
if err := intro.Unmarshal(data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for name, key := range d.pki.People {
|
||||
if *key == intro.LongTermKey {
|
||||
d.gui.Warnf("Received introduction: %s\n", name)
|
||||
continue OUTER
|
||||
}
|
||||
}
|
||||
d.gui.Warnf("Received introduction: (%s)\n", &intro.LongTermKey)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/jroimartin/gocui"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
)
|
||||
|
||||
type GuiClient struct {
|
||||
sync.Mutex
|
||||
|
||||
pki *PKI
|
||||
myName string
|
||||
myPublicKey *BoxKey
|
||||
myPrivateKey *BoxKey
|
||||
|
||||
gui *gocui.Gui
|
||||
client *Client
|
||||
|
||||
selectedConvo *Conversation
|
||||
conversations map[string]*Conversation
|
||||
dialer *Dialer
|
||||
}
|
||||
|
||||
func (gc *GuiClient) switchConversation(peer string) {
|
||||
var convo *Conversation
|
||||
|
||||
convo, ok := gc.conversations[peer]
|
||||
if !ok {
|
||||
peerPublicKey, ok := gc.pki.People[peer]
|
||||
if !ok {
|
||||
gc.Warnf("unknown user: %s", peer)
|
||||
return
|
||||
}
|
||||
convo = &Conversation{
|
||||
pki: gc.pki,
|
||||
peerName: peer,
|
||||
peerPublicKey: peerPublicKey,
|
||||
myPublicKey: gc.myPublicKey,
|
||||
myPrivateKey: gc.myPrivateKey,
|
||||
gui: gc,
|
||||
}
|
||||
convo.Init()
|
||||
gc.conversations[peer] = convo
|
||||
}
|
||||
|
||||
gc.selectedConvo = convo
|
||||
gc.activateConvo(convo)
|
||||
gc.Warnf("Now talking to %s\n", peer)
|
||||
}
|
||||
|
||||
func (gc *GuiClient) activateConvo(convo *Conversation) {
|
||||
if gc.client != nil {
|
||||
convo.Lock()
|
||||
convo.lastPeerResponding = false
|
||||
convo.lastLatency = 0
|
||||
convo.Unlock()
|
||||
gc.client.SetConvoHandler(convo)
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *GuiClient) handleLine(line string) error {
|
||||
switch {
|
||||
case line == "/quit":
|
||||
return gocui.Quit
|
||||
case strings.HasPrefix(line, "/talk "):
|
||||
peer := line[6:]
|
||||
gc.switchConversation(peer)
|
||||
case strings.HasPrefix(line, "/dial "):
|
||||
peer := line[6:]
|
||||
pk, ok := gc.pki.People[peer]
|
||||
if !ok {
|
||||
gc.Warnf("Unknown user: %q (see %s)\n", peer, *pkiPath)
|
||||
return nil
|
||||
}
|
||||
gc.Warnf("Dialing user: %s\n", peer)
|
||||
gc.dialer.QueueRequest(pk)
|
||||
default:
|
||||
msg := strings.TrimSpace(line)
|
||||
gc.selectedConvo.QueueTextMessage([]byte(msg))
|
||||
gc.Printf("<%s> %s\n", gc.myName, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *GuiClient) readLine(_ *gocui.Gui, v *gocui.View) error {
|
||||
// HACK: pressing enter on startup causes panic
|
||||
if len(v.Buffer()) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, cy := v.Cursor()
|
||||
line, err := v.Line(cy - 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if line == "" {
|
||||
return nil
|
||||
}
|
||||
v.Clear()
|
||||
|
||||
return gc.handleLine(line)
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Flush() {
|
||||
gc.gui.Flush()
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Warnf(format string, v ...interface{}) {
|
||||
mv, err := gc.gui.View("main")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(mv, "-!- "+format, v...)
|
||||
gc.gui.Flush()
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Printf(format string, v ...interface{}) {
|
||||
mv, err := gc.gui.View("main")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(mv, format, v...)
|
||||
gc.gui.Flush()
|
||||
}
|
||||
|
||||
func (gc *GuiClient) layout(g *gocui.Gui) error {
|
||||
maxX, maxY := g.Size()
|
||||
if v, err := g.SetView("main", 0, -1, maxX-1, maxY-1); err != nil {
|
||||
if err != gocui.ErrorUnkView {
|
||||
return err
|
||||
}
|
||||
v.Autoscroll = true
|
||||
v.Wrap = true
|
||||
v.Frame = false
|
||||
log.AddHook(gc)
|
||||
log.SetOutput(ioutil.Discard)
|
||||
log.SetFormatter(&GuiFormatter{})
|
||||
}
|
||||
sv, err := g.SetView("status", -1, maxY-3, maxX, maxY-1)
|
||||
if err != nil {
|
||||
if err != gocui.ErrorUnkView {
|
||||
return err
|
||||
}
|
||||
sv.Wrap = false
|
||||
sv.Frame = false
|
||||
sv.BgColor = gocui.ColorBlue
|
||||
sv.FgColor = gocui.ColorWhite
|
||||
}
|
||||
sv.Clear()
|
||||
|
||||
st := gc.selectedConvo.Status()
|
||||
latency := fmt.Sprintf("%.1fs", st.Latency)
|
||||
if st.Latency == 0.0 {
|
||||
latency = "-"
|
||||
}
|
||||
round := fmt.Sprintf("%d", st.Round)
|
||||
if st.Round == 0 {
|
||||
round = "-"
|
||||
}
|
||||
fmt.Fprintf(sv, " [%s] [round: %s] [latency: %s]", gc.myName, round, latency)
|
||||
|
||||
partner := "(no partner)"
|
||||
if !gc.selectedConvo.Solo() {
|
||||
partner = gc.selectedConvo.peerName
|
||||
}
|
||||
|
||||
pv, err := g.SetView("partner", -1, maxY-2, len(partner)+1, maxY)
|
||||
if err != nil {
|
||||
if err != gocui.ErrorUnkView {
|
||||
return err
|
||||
}
|
||||
pv.Wrap = false
|
||||
pv.Frame = false
|
||||
}
|
||||
pv.Clear()
|
||||
|
||||
if st.PeerResponding {
|
||||
pv.FgColor = gocui.ColorGreen
|
||||
} else {
|
||||
pv.FgColor = gocui.ColorRed
|
||||
}
|
||||
fmt.Fprintf(pv, "%s>", partner)
|
||||
|
||||
if v, err := g.SetView("input", len(partner)+1, maxY-2, maxX, maxY); err != nil {
|
||||
if err != gocui.ErrorUnkView {
|
||||
return err
|
||||
}
|
||||
v.Editable = true
|
||||
v.Wrap = false
|
||||
v.Frame = false
|
||||
if err := g.SetCurrentView("input"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.Quit
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Connect() error {
|
||||
if gc.client == nil {
|
||||
gc.client = NewClient(gc.pki.EntryServer, gc.myPublicKey)
|
||||
gc.client.SetDialHandler(gc.dialer)
|
||||
}
|
||||
gc.activateConvo(gc.selectedConvo)
|
||||
return gc.client.Connect()
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Run() {
|
||||
gui := gocui.NewGui()
|
||||
if err := gui.Init(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
defer gui.Close()
|
||||
gc.gui = gui
|
||||
|
||||
if err := gui.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err := gui.SetKeybinding("input", gocui.KeyEnter, gocui.ModNone, gc.readLine); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
gui.ShowCursor = true
|
||||
gui.BgColor = gocui.ColorDefault
|
||||
gui.FgColor = gocui.ColorDefault
|
||||
gui.SetLayout(gc.layout)
|
||||
|
||||
gc.conversations = make(map[string]*Conversation)
|
||||
gc.switchConversation(gc.myName)
|
||||
|
||||
gc.dialer = &Dialer{
|
||||
gui: gc,
|
||||
pki: gc.pki,
|
||||
myPublicKey: gc.myPublicKey,
|
||||
myPrivateKey: gc.myPrivateKey,
|
||||
}
|
||||
gc.dialer.Init()
|
||||
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if err := gc.Connect(); err != nil {
|
||||
gc.Warnf("Failed to connect: %s\n", err)
|
||||
}
|
||||
gc.Warnf("Connected: %s\n", gc.pki.EntryServer)
|
||||
}()
|
||||
|
||||
err := gui.MainLoop()
|
||||
if err != nil && err != gocui.Quit {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Fire(entry *log.Entry) error {
|
||||
line, err := entry.String()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gc.Warnf(line)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gc *GuiClient) Levels() []log.Level {
|
||||
return []log.Level{
|
||||
log.PanicLevel,
|
||||
log.FatalLevel,
|
||||
log.ErrorLevel,
|
||||
log.WarnLevel,
|
||||
log.InfoLevel,
|
||||
log.DebugLevel,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
)
|
||||
|
||||
var doInit = flag.Bool("init", false, "create default config file")
|
||||
var confPath = flag.String("conf", "confs/client.conf", "config file")
|
||||
var pkiPath = flag.String("pki", "confs/pki.conf", "pki file")
|
||||
|
||||
type Conf struct {
|
||||
MyName string
|
||||
MyPublicKey *BoxKey
|
||||
MyPrivateKey *BoxKey
|
||||
}
|
||||
|
||||
func WriteDefaultConf(path string) {
|
||||
myPublicKey, myPrivateKey, err := GenerateBoxKey(rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatalf("GenerateBoxKey: %s", err)
|
||||
}
|
||||
conf := &Conf{
|
||||
MyPublicKey: myPublicKey,
|
||||
MyPrivateKey: myPrivateKey,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("json encoding error: %s", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
||||
log.Fatalf("WriteFile: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *doInit {
|
||||
WriteDefaultConf(*confPath)
|
||||
return
|
||||
}
|
||||
|
||||
pki := ReadPKI(*pkiPath)
|
||||
|
||||
conf := new(Conf)
|
||||
ReadJSONFile(*confPath, conf)
|
||||
if conf.MyName == "" || conf.MyPublicKey == nil || conf.MyPrivateKey == nil {
|
||||
log.Fatalf("missing required fields: %s", *confPath)
|
||||
}
|
||||
|
||||
gc := &GuiClient{
|
||||
pki: pki,
|
||||
myName: conf.MyName,
|
||||
myPublicKey: conf.MyPublicKey,
|
||||
myPrivateKey: conf.MyPrivateKey,
|
||||
}
|
||||
gc.Run()
|
||||
}
|
|
@ -0,0 +1,363 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/vrpc"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
connectionsMu sync.Mutex
|
||||
connections map[*connection]bool
|
||||
|
||||
convoMu sync.Mutex
|
||||
convoRound uint32
|
||||
convoRequests []*convoReq
|
||||
|
||||
dialMu sync.Mutex
|
||||
dialRound uint32
|
||||
dialRequests []*dialReq
|
||||
|
||||
firstServer *vrpc.Client
|
||||
lastServer *vrpc.Client
|
||||
}
|
||||
|
||||
type convoReq struct {
|
||||
conn *connection
|
||||
onion []byte
|
||||
}
|
||||
|
||||
type dialReq struct {
|
||||
conn *connection
|
||||
onion []byte
|
||||
}
|
||||
|
||||
type connection struct {
|
||||
sync.Mutex
|
||||
|
||||
ws *websocket.Conn
|
||||
srv *server
|
||||
publicKey *BoxKey
|
||||
}
|
||||
|
||||
func (srv *server) register(c *connection) {
|
||||
srv.connectionsMu.Lock()
|
||||
srv.connections[c] = true
|
||||
srv.connectionsMu.Unlock()
|
||||
}
|
||||
|
||||
func (srv *server) allConnections() []*connection {
|
||||
srv.connectionsMu.Lock()
|
||||
conns := make([]*connection, len(srv.connections))
|
||||
i := 0
|
||||
for c := range srv.connections {
|
||||
conns[i] = c
|
||||
i++
|
||||
}
|
||||
srv.connectionsMu.Unlock()
|
||||
return conns
|
||||
}
|
||||
|
||||
func broadcast(conns []*connection, v interface{}) {
|
||||
ParallelFor(len(conns), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
conns[i].Send(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *connection) Close() {
|
||||
c.ws.Close()
|
||||
|
||||
c.srv.connectionsMu.Lock()
|
||||
delete(c.srv.connections, c)
|
||||
c.srv.connectionsMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *connection) Send(v interface{}) {
|
||||
const writeWait = 10 * time.Second
|
||||
|
||||
e, err := Envelop(v)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"bug": true, "call": "Envelop"}).Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := c.ws.WriteJSON(e); err != nil {
|
||||
log.WithFields(log.Fields{"call": "WriteJSON"}).Debug(err)
|
||||
c.Unlock()
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *connection) readLoop() {
|
||||
for {
|
||||
var e Envelope
|
||||
if err := c.ws.ReadJSON(&e); err != nil {
|
||||
log.WithFields(log.Fields{"call": "ReadJSON"}).Debug(err)
|
||||
c.Close()
|
||||
break
|
||||
}
|
||||
|
||||
v, err := e.Open()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error parsing request: %s", err)
|
||||
go c.Send(&BadRequestError{Err: msg})
|
||||
}
|
||||
go c.handleRequest(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) handleRequest(v interface{}) {
|
||||
switch v := v.(type) {
|
||||
case *ConvoRequest:
|
||||
c.handleConvoRequest(v)
|
||||
case *DialRequest:
|
||||
c.handleDialRequest(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *connection) handleConvoRequest(r *ConvoRequest) {
|
||||
srv := c.srv
|
||||
srv.convoMu.Lock()
|
||||
currRound := srv.convoRound
|
||||
if r.Round != currRound {
|
||||
srv.convoMu.Unlock()
|
||||
err := fmt.Sprintf("wrong round (currently %d)", currRound)
|
||||
go c.Send(&ConvoError{Round: r.Round, Err: err})
|
||||
return
|
||||
}
|
||||
rr := &convoReq{
|
||||
conn: c,
|
||||
onion: r.Onion,
|
||||
}
|
||||
srv.convoRequests = append(srv.convoRequests, rr)
|
||||
srv.convoMu.Unlock()
|
||||
}
|
||||
|
||||
func (c *connection) handleDialRequest(r *DialRequest) {
|
||||
srv := c.srv
|
||||
srv.dialMu.Lock()
|
||||
currRound := srv.dialRound
|
||||
if r.Round != currRound {
|
||||
srv.dialMu.Unlock()
|
||||
err := fmt.Sprintf("wrong round (currently %d)", currRound)
|
||||
go c.Send(&DialError{Round: r.Round, Err: err})
|
||||
return
|
||||
}
|
||||
rr := &dialReq{
|
||||
conn: c,
|
||||
onion: r.Onion,
|
||||
}
|
||||
srv.dialRequests = append(srv.dialRequests, rr)
|
||||
srv.dialMu.Unlock()
|
||||
}
|
||||
|
||||
func (srv *server) convoRoundLoop() {
|
||||
for {
|
||||
if err := NewConvoRound(srv.firstServer, srv.convoRound); err != nil {
|
||||
log.WithFields(log.Fields{"service": "convo", "round": srv.convoRound, "call": "NewConvoRound"}).Error(err)
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.WithFields(log.Fields{"service": "convo", "round": srv.convoRound}).Info("Broadcast")
|
||||
|
||||
broadcast(srv.allConnections(), &AnnounceConvoRound{srv.convoRound})
|
||||
time.Sleep(*receiveWait)
|
||||
|
||||
srv.convoMu.Lock()
|
||||
go srv.runConvoRound(srv.convoRound, srv.convoRequests)
|
||||
|
||||
srv.convoRound += 1
|
||||
srv.convoRequests = make([]*convoReq, 0, len(srv.convoRequests))
|
||||
srv.convoMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) dialRoundLoop() {
|
||||
for {
|
||||
time.Sleep(DialWait)
|
||||
if err := NewDialRound(srv.firstServer, srv.dialRound); err != nil {
|
||||
log.WithFields(log.Fields{"service": "dial", "round": srv.dialRound, "call": "NewDialRound"}).Error(err)
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.WithFields(log.Fields{"service": "dial", "round": srv.dialRound}).Info("Broadcast")
|
||||
|
||||
broadcast(srv.allConnections(), &AnnounceDialRound{srv.dialRound, TotalDialBuckets})
|
||||
time.Sleep(*receiveWait)
|
||||
|
||||
srv.dialMu.Lock()
|
||||
go srv.runDialRound(srv.dialRound, srv.dialRequests)
|
||||
|
||||
srv.dialRound += 1
|
||||
srv.dialRequests = make([]*dialReq, 0, len(srv.dialRequests))
|
||||
srv.dialMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *server) runConvoRound(round uint32, requests []*convoReq) {
|
||||
conns := make([]*connection, len(requests))
|
||||
onions := make([][]byte, len(requests))
|
||||
for i, r := range requests {
|
||||
conns[i] = r.conn
|
||||
onions[i] = r.onion
|
||||
}
|
||||
|
||||
rlog := log.WithFields(log.Fields{"service": "convo", "round": round})
|
||||
rlog.WithFields(log.Fields{"call": "RunConvoRound", "onions": len(onions)}).Info()
|
||||
|
||||
replies, err := RunConvoRound(srv.firstServer, round, onions)
|
||||
if err != nil {
|
||||
rlog.WithFields(log.Fields{"call": "RunConvoRound"}).Error(err)
|
||||
broadcast(conns, &ConvoError{Round: round, Err: "server error"})
|
||||
return
|
||||
}
|
||||
|
||||
rlog.WithFields(log.Fields{"replies": len(replies)}).Info("Success")
|
||||
|
||||
ParallelFor(len(replies), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
reply := &ConvoResponse{
|
||||
Round: round,
|
||||
Onion: replies[i],
|
||||
}
|
||||
conns[i].Send(reply)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (srv *server) runDialRound(round uint32, requests []*dialReq) {
|
||||
conns := make([]*connection, len(requests))
|
||||
onions := make([][]byte, len(requests))
|
||||
for i, r := range requests {
|
||||
conns[i] = r.conn
|
||||
onions[i] = r.onion
|
||||
}
|
||||
|
||||
rlog := log.WithFields(log.Fields{"service": "dial", "round": round})
|
||||
rlog.WithFields(log.Fields{"call": "RunDialRound", "onions": len(onions)}).Info()
|
||||
|
||||
if err := RunDialRound(srv.firstServer, round, onions); err != nil {
|
||||
rlog.WithFields(log.Fields{"call": "RunDialRound"}).Error(err)
|
||||
broadcast(conns, &DialError{Round: round, Err: "server error"})
|
||||
return
|
||||
}
|
||||
|
||||
args := &DialBucketsArgs{Round: round}
|
||||
result := new(DialBucketsResult)
|
||||
if err := srv.lastServer.Call("DialService.Buckets", args, result); err != nil {
|
||||
rlog.WithFields(log.Fields{"call": "Buckets"}).Error(err)
|
||||
broadcast(conns, &DialError{Round: round, Err: "server error"})
|
||||
return
|
||||
}
|
||||
|
||||
intros := 0
|
||||
for _, b := range result.Buckets {
|
||||
intros += len(b)
|
||||
}
|
||||
rlog.WithFields(log.Fields{"buckets": len(result.Buckets), "intros": intros}).Info("Buckets")
|
||||
|
||||
ParallelFor(len(conns), func(p *P) {
|
||||
for i, ok := p.Next(); ok; i, ok = p.Next() {
|
||||
c := conns[i]
|
||||
bi := KeyDialBucket(c.publicKey, TotalDialBuckets)
|
||||
|
||||
db := &DialBucket{
|
||||
Round: round,
|
||||
Intros: result.Buckets[bi],
|
||||
}
|
||||
c.Send(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func (srv *server) wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
pk, err := KeyFromString(r.URL.Query().Get("publickey"))
|
||||
if err != nil {
|
||||
http.Error(w, "expecting box key in publickey query parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Upgrade: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
c := &connection{
|
||||
ws: ws,
|
||||
srv: srv,
|
||||
publicKey: pk,
|
||||
}
|
||||
srv.register(c)
|
||||
c.readLoop()
|
||||
}
|
||||
|
||||
var addr = flag.String("addr", ":8080", "http service address")
|
||||
var pkiPath = flag.String("pki", "confs/pki.conf", "pki file")
|
||||
var receiveWait = flag.Duration("wait", DefaultReceiveWait, "")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFormatter(&ServerFormatter{})
|
||||
|
||||
pki := ReadPKI(*pkiPath)
|
||||
|
||||
firstServer, err := vrpc.Dial("tcp", pki.FirstServer(), runtime.NumCPU())
|
||||
if err != nil {
|
||||
log.Fatalf("vrpc.Dial: %s", err)
|
||||
}
|
||||
|
||||
lastServer, err := vrpc.Dial("tcp", pki.LastServer(), 1)
|
||||
if err != nil {
|
||||
log.Fatalf("vrpc.Dial: %s", err)
|
||||
}
|
||||
|
||||
srv := &server{
|
||||
firstServer: firstServer,
|
||||
lastServer: lastServer,
|
||||
connections: make(map[*connection]bool),
|
||||
convoRound: 0,
|
||||
convoRequests: make([]*convoReq, 0, 10000),
|
||||
dialRound: 0,
|
||||
dialRequests: make([]*dialReq, 0, 10000),
|
||||
}
|
||||
|
||||
go srv.convoRoundLoop()
|
||||
go srv.dialRoundLoop()
|
||||
|
||||
http.HandleFunc("/ws", srv.wsHandler)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: *addr,
|
||||
}
|
||||
|
||||
if err := httpServer.ListenAndServe(); err != nil {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"gopkg.in/gizak/termui.v1"
|
||||
|
||||
"github.com/davidlazar/vuvuzela"
|
||||
)
|
||||
|
||||
type Histogram struct {
|
||||
Mu float64
|
||||
NumServers int
|
||||
|
||||
singles []int
|
||||
doubles []int
|
||||
|
||||
normalizedSingles []int
|
||||
normalizedDoubles []int
|
||||
|
||||
spSingles *termui.Sparklines
|
||||
spDoubles *termui.Sparklines
|
||||
}
|
||||
|
||||
func (h *Histogram) resize() {
|
||||
tw := termui.TermWidth() - 4
|
||||
h.spSingles.Width = tw
|
||||
h.spDoubles.Width = tw
|
||||
|
||||
th := termui.TermHeight()/2 - 1
|
||||
h.spSingles.Height = th
|
||||
h.spDoubles.Height = th
|
||||
if th > 3 {
|
||||
h.spSingles.Lines[0].Height = th - 3
|
||||
h.spDoubles.Lines[0].Height = th - 3
|
||||
} else {
|
||||
h.spSingles.Lines[0].Height = 1
|
||||
h.spDoubles.Lines[0].Height = 1
|
||||
}
|
||||
|
||||
h.spDoubles.Y = th + 2
|
||||
|
||||
termui.Body.Width = termui.TermWidth()
|
||||
termui.Body.Align()
|
||||
h.render()
|
||||
}
|
||||
|
||||
// Shift the distribution so the user can more clearly see the variation
|
||||
// in noise across rounds.
|
||||
func (h *Histogram) render() {
|
||||
singleShift := (h.NumServers-1)*int(h.Mu) - h.spSingles.Height - 32
|
||||
doubleShift := (h.NumServers-1)*int(h.Mu/2) - h.spDoubles.Height - 16
|
||||
|
||||
for i := range h.singles {
|
||||
if s := h.singles[i]; s == 0 {
|
||||
h.normalizedSingles[i] = 0
|
||||
} else if n := s - singleShift; n > 2 {
|
||||
h.normalizedSingles[i] = n
|
||||
} else {
|
||||
// to prevent confusion, don't let the sparkline go to 0
|
||||
h.normalizedSingles[i] = 2
|
||||
}
|
||||
|
||||
if s := h.doubles[i]; s == 0 {
|
||||
h.normalizedDoubles[i] = 0
|
||||
} else if n := s - doubleShift; n > 2 {
|
||||
h.normalizedDoubles[i] = n
|
||||
} else {
|
||||
h.normalizedDoubles[i] = 2
|
||||
}
|
||||
}
|
||||
|
||||
h.spSingles.Lines[0].Data = h.normalizedSingles
|
||||
h.spDoubles.Lines[0].Data = h.normalizedDoubles
|
||||
termui.Render(h.spSingles, h.spDoubles)
|
||||
}
|
||||
|
||||
func (h *Histogram) run(accessCounts chan *vuvuzela.AccessCount) {
|
||||
h.singles = make([]int, 512)
|
||||
h.doubles = make([]int, 512)
|
||||
h.normalizedSingles = make([]int, 512)
|
||||
h.normalizedDoubles = make([]int, 512)
|
||||
|
||||
// log will corrupt display, so only log errors
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
|
||||
err := termui.Init()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer termui.Close()
|
||||
|
||||
termui.UseTheme("helloworld")
|
||||
th := termui.Theme()
|
||||
th.BodyBg = termui.ColorDefault
|
||||
th.BlockBg = termui.ColorDefault
|
||||
th.BorderBg = termui.ColorDefault
|
||||
th.BorderLabelTextBg = termui.ColorDefault
|
||||
termui.SetTheme(th)
|
||||
|
||||
spSingles := termui.NewSparkline()
|
||||
spSingles.Data = h.singles
|
||||
spSingles.LineColor = termui.ColorBlue
|
||||
|
||||
spDoubles := termui.NewSparkline()
|
||||
spDoubles.Data = h.doubles
|
||||
spDoubles.LineColor = termui.ColorMagenta
|
||||
|
||||
h.spSingles = termui.NewSparklines(spSingles)
|
||||
h.spSingles.X = 2
|
||||
h.spSingles.Y = 1
|
||||
h.spSingles.Border.Label = "Idle Users"
|
||||
|
||||
h.spDoubles = termui.NewSparklines(spDoubles)
|
||||
h.spDoubles.X = 2
|
||||
h.spDoubles.Border.Label = "Active Users"
|
||||
|
||||
h.resize()
|
||||
|
||||
for {
|
||||
select {
|
||||
case e := <-termui.EventCh():
|
||||
if e.Type == termui.EventKey && e.Ch == 'q' {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
return
|
||||
}
|
||||
if e.Type == termui.EventKey && e.Key == termui.KeyCtrlC {
|
||||
termui.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
if e.Type == termui.EventResize {
|
||||
h.resize()
|
||||
}
|
||||
case a := <-accessCounts:
|
||||
h.singles = append(h.singles[1:], int(a.Singles))
|
||||
h.doubles = append(h.doubles[1:], int(a.Doubles))
|
||||
//log.Errorf("%#v", a)
|
||||
h.render()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"net/rpc"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
. "github.com/davidlazar/vuvuzela"
|
||||
. "github.com/davidlazar/vuvuzela/internal"
|
||||
"github.com/davidlazar/vuvuzela/vrpc"
|
||||
)
|
||||
|
||||
var doInit = flag.Bool("init", false, "create default config file")
|
||||
var confPath = flag.String("conf", "", "config file")
|
||||
var pkiPath = flag.String("pki", "confs/pki.conf", "pki file")
|
||||
var muOverride = flag.Float64("mu", -1.0, "override ConvoMu in conf file")
|
||||
|
||||
type Conf struct {
|
||||
ServerName string
|
||||
PublicKey *BoxKey
|
||||
PrivateKey *BoxKey
|
||||
ListenAddr string `json:",omitempty"`
|
||||
DebugAddr string `json:",omitempty"`
|
||||
|
||||
ConvoMu float64
|
||||
ConvoB float64
|
||||
|
||||
DialMu float64
|
||||
DialB float64
|
||||
}
|
||||
|
||||
func WriteDefaultConf(path string) {
|
||||
myPublicKey, myPrivateKey, err := GenerateBoxKey(rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatalf("GenerateKey: %s", err)
|
||||
}
|
||||
conf := &Conf{
|
||||
ServerName: "mit",
|
||||
PublicKey: myPublicKey,
|
||||
PrivateKey: myPrivateKey,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("json encoding error: %s", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
||||
log.Fatalf("WriteFile: %s", err)
|
||||
}
|
||||
fmt.Printf("wrote %q\n", path)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.SetFormatter(&ServerFormatter{})
|
||||
|
||||
if *confPath == "" {
|
||||
log.Fatalf("must specify -conf flag")
|
||||
}
|
||||
|
||||
if *doInit {
|
||||
WriteDefaultConf(*confPath)
|
||||
return
|
||||
}
|
||||
|
||||
pki := ReadPKI(*pkiPath)
|
||||
|
||||
conf := new(Conf)
|
||||
ReadJSONFile(*confPath, conf)
|
||||
if conf.ServerName == "" || conf.PublicKey == nil || conf.PrivateKey == nil {
|
||||
log.Fatalf("missing required fields: %s", *confPath)
|
||||
}
|
||||
|
||||
if *muOverride >= 0 {
|
||||
conf.ConvoMu = *muOverride
|
||||
}
|
||||
|
||||
var err error
|
||||
var client *vrpc.Client
|
||||
if addr := pki.NextServer(conf.ServerName); addr != "" {
|
||||
client, err = vrpc.Dial("tcp", addr, runtime.NumCPU())
|
||||
if err != nil {
|
||||
log.Fatalf("vrpc.Dial: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
var idle sync.Mutex
|
||||
|
||||
convoService := &ConvoService{
|
||||
Idle: &idle,
|
||||
|
||||
LaplaceMu: conf.ConvoMu,
|
||||
LaplaceB: conf.ConvoB,
|
||||
|
||||
PKI: pki,
|
||||
ServerName: conf.ServerName,
|
||||
PrivateKey: conf.PrivateKey,
|
||||
|
||||
Client: client,
|
||||
LastServer: client == nil,
|
||||
}
|
||||
InitConvoService(convoService)
|
||||
|
||||
if convoService.LastServer {
|
||||
histogram := &Histogram{Mu: conf.ConvoMu, NumServers: len(pki.ServerOrder)}
|
||||
go histogram.run(convoService.AccessCounts)
|
||||
}
|
||||
|
||||
dialService := &DialService{
|
||||
Idle: &idle,
|
||||
|
||||
LaplaceMu: conf.DialMu,
|
||||
LaplaceB: conf.DialB,
|
||||
|
||||
PKI: pki,
|
||||
ServerName: conf.ServerName,
|
||||
PrivateKey: conf.PrivateKey,
|
||||
|
||||
Client: client,
|
||||
LastServer: client == nil,
|
||||
}
|
||||
InitDialService(dialService)
|
||||
|
||||
if err := rpc.Register(dialService); err != nil {
|
||||
log.Fatalf("rpc.Register: %s", err)
|
||||
}
|
||||
if err := rpc.Register(convoService); err != nil {
|
||||
log.Fatalf("rpc.Register: %s", err)
|
||||
}
|
||||
|
||||
if conf.DebugAddr != "" {
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe(conf.DebugAddr, nil))
|
||||
}()
|
||||
runtime.SetBlockProfileRate(1)
|
||||
}
|
||||
|
||||
if conf.ListenAddr == "" {
|
||||
conf.ListenAddr = DefaultServerAddr
|
||||
}
|
||||
listen, err := net.Listen("tcp", conf.ListenAddr)
|
||||
if err != nil {
|
||||
log.Fatal("Listen:", err)
|
||||
}
|
||||
rpc.Accept(listen)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
"github.com/davidlazar/vuvuzela/onionbox"
|
||||
)
|
||||
|
||||
type DeadDrop [16]byte
|
||||
|
||||
const (
|
||||
SizeEncryptedMessage = SizeMessage + box.Overhead
|
||||
SizeConvoExchange = int(unsafe.Sizeof(ConvoExchange{}))
|
||||
SizeEncryptedIntro = int(unsafe.Sizeof(Introduction{})) + onionbox.Overhead
|
||||
SizeDialExchange = int(unsafe.Sizeof(DialExchange{}))
|
||||
)
|
||||
|
||||
type ConvoExchange struct {
|
||||
DeadDrop DeadDrop
|
||||
EncryptedMessage [SizeEncryptedMessage]byte
|
||||
}
|
||||
|
||||
func (e *ConvoExchange) Marshal() []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.BigEndian, e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (e *ConvoExchange) Unmarshal(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
return binary.Read(buf, binary.BigEndian, e)
|
||||
}
|
||||
|
||||
type Introduction struct {
|
||||
Rendezvous uint32
|
||||
LongTermKey BoxKey
|
||||
}
|
||||
|
||||
func (i *Introduction) Marshal() []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.BigEndian, i); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (i *Introduction) Unmarshal(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
return binary.Read(buf, binary.BigEndian, i)
|
||||
}
|
||||
|
||||
type DialExchange struct {
|
||||
Bucket uint32
|
||||
EncryptedIntro [SizeEncryptedIntro]byte
|
||||
}
|
||||
|
||||
func (e *DialExchange) Marshal() []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := binary.Write(buf, binary.BigEndian, e); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (e *DialExchange) Unmarshal(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
return binary.Read(buf, binary.BigEndian, e)
|
||||
}
|
||||
|
||||
func ForwardNonce(round uint32) *[24]byte {
|
||||
var nonce [24]byte
|
||||
binary.BigEndian.PutUint32(nonce[0:4], round)
|
||||
nonce[4] = 0
|
||||
return &nonce
|
||||
}
|
||||
|
||||
func BackwardNonce(round uint32) *[24]byte {
|
||||
var nonce [24]byte
|
||||
binary.BigEndian.PutUint32(nonce[0:4], round)
|
||||
nonce[4] = 1
|
||||
return &nonce
|
||||
}
|
||||
|
||||
func KeyDialBucket(key *BoxKey, buckets uint32) uint32 {
|
||||
return binary.BigEndian.Uint32(key[28:32]) % buckets
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package vuvuzela
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDialExchangeMarshal(t *testing.T) {
|
||||
ex := new(DialExchange)
|
||||
_ = ex.Marshal()
|
||||
}
|
Loading…
Reference in New Issue