Publish Vuvuzela v0.1

This commit is contained in:
David Lazar 2015-12-01 22:59:31 -05:00
parent d54f8deb74
commit cf61af556e
50 changed files with 3900 additions and 4 deletions

15
LICENSE Normal file
View File

@ -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
View File

@ -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.

66
boxkey.go Normal file
View File

@ -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
}

5
confs/alice.conf Normal file
View File

@ -0,0 +1,5 @@
{
"MyName": "alice",
"MyPublicKey": "j10hpqtgnqc1y21xp5y7yamwa32jvdp89888q2semnxg95j4v82g",
"MyPrivateKey": "82v7008ke1dyzatzq04mrtnxt5s92vnfxpdgr61rbtw30hbge330"
}

5
confs/bob.conf Normal file
View File

@ -0,0 +1,5 @@
{
"MyName": "bob",
"MyPublicKey": "nhd9ja88j65zwmnszw0b12zg1wqgqqmq382tafyw3gd642e9s1ag",
"MyPrivateKey": "wqdzrmdyvk7ee8w37r8ey56pt5dkkfz64039qhzv3w81a75bgsc0"
}

10
confs/local-first.conf Normal file
View File

@ -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
}

11
confs/local-last.conf Normal file
View File

@ -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
}

10
confs/local-middle.conf Normal file
View File

@ -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
}

22
confs/pki.conf Normal file
View File

@ -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"
}

425
convo.go Normal file
View File

@ -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
}

263
dial.go Normal file
View File

@ -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
}

147
entry_api.go Normal file
View File

@ -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
}

65
internal/ansi.go Normal file
View File

@ -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")
}

1
internal/box/box.go Normal file
View File

@ -0,0 +1 @@
package box

31
internal/box/box_test.go Normal file
View File

@ -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)
}
}

19
internal/json.go Normal file
View File

@ -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)
}
}

110
internal/log.go Normal file
View File

@ -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(' ')
}
}

74
internal/parallelfor.go Normal file
View File

@ -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()
}

20
internal/span.go Normal file
View File

@ -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
}

31
internal/span_test.go Normal file
View File

@ -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
}

38
laplace.go Normal file
View File

@ -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)
}

16
msgtype_string.go Normal file
View File

@ -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]]
}

57
noise.go Normal file
View File

@ -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
}
})
}

54
noise_test.go Normal file
View File

@ -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
}

40
onionbox/onion.go Normal file
View File

@ -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
}

19
params.go Normal file
View File

@ -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"
)

106
pki.go Normal file
View File

@ -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
}

41
pki_test.go Normal file
View File

@ -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")
}
}

1
rand/README.md Normal file
View File

@ -0,0 +1 @@
`cpu*` code from https://github.com/jonhoo/drwmutex

11
rand/cpu.go Normal file
View File

@ -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
}

3
rand/cpu_amd64.go Normal file
View File

@ -0,0 +1,3 @@
package rand
func cpu() uint64

15
rand/cpu_amd64.s Normal file
View File

@ -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

107
rand/salsa20rand.go Normal file
View File

@ -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)
}

109
rand/salsa20rand_test.go Normal file
View File

@ -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)
}
}
}

BIN
screenshots/client.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
screenshots/server.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 KiB

45
shuffle.go Normal file
View File

@ -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))
}
}
}

38
shuffle_test.go Normal file
View File

@ -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
}
}
}

81
vrpc/client.go Normal file
View File

@ -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
}

156
vuvuzela-client/client.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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")
}
}

82
vuvuzela-client/dialer.go Normal file
View File

@ -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)
}
}

283
vuvuzela-client/gui.go Normal file
View File

@ -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,
}
}

67
vuvuzela-client/main.go Normal file
View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

156
vuvuzela-server/server.go Normal file
View File

@ -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)
}

92
vuvuzela.go Normal file
View File

@ -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
}

10
vuvuzela_test.go Normal file
View File

@ -0,0 +1,10 @@
package vuvuzela
import (
"testing"
)
func TestDialExchangeMarshal(t *testing.T) {
ex := new(DialExchange)
_ = ex.Marshal()
}