├── .github └── mailbot.json ├── log ├── ansi │ ├── ansi_test.go │ └── ansi.go ├── output.go ├── LICENSE ├── global.go ├── levels.go ├── log.go ├── bench_test.go └── format.go ├── encoding └── toml │ ├── toml.go │ ├── lexer_test.go │ ├── LICENSE │ ├── decode.go │ ├── parser_test.go │ ├── parser.y │ └── decode_test.go ├── cmd ├── cmdutil │ └── util.go ├── guardian │ ├── alpenhorn-guardian-upload-config │ │ └── main.go │ ├── alpenhorn-guardian-new-config │ │ └── main.go │ ├── alpenhorn-guardian-send-announcement │ │ └── main.go │ ├── alpenhorn-guardian-sign-config │ │ └── main.go │ ├── guardian.go │ └── alpenhorn-guardian-keygen │ │ └── main.go ├── alpenhorn-config-server │ └── main.go ├── alpenhorn-cdn │ └── main.go ├── alpenhorn-mixer │ └── main.go └── alpenhorn-pkg │ └── main.go ├── pkg ├── errorcode_string.go ├── data_test.go ├── extract_test.go ├── register_test.go ├── server_easyjson.go ├── errors.go ├── status.go ├── register.go ├── data.go └── client.go ├── bootstrap.go ├── LICENSE ├── README.md ├── internal ├── pg │ └── pg.go └── alplog │ └── format.go ├── edtls ├── LICENSE ├── client_test.go ├── doc.go ├── client.go └── server.go ├── mailbox.go ├── errors ├── errors.go └── LICENSE ├── typesocket ├── mux.go ├── typesocket_test.go ├── client.go └── hub.go ├── bloom ├── LICENSE ├── bloom.go └── bloom_test.go ├── coordinator └── persist.go ├── intro.go ├── config ├── persist.go ├── server_test.go ├── client.go └── server.go ├── keywheel ├── keywheel_easyjson.go ├── keywheel_test.go └── keywheel.go ├── edhttp └── client.go ├── cdn └── cdn_test.go ├── friendrequest.go ├── persist.go ├── dialing └── mixer.go ├── dialing.go ├── addfriend └── mixer.go └── friend.go /.github/mailbot.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitEmailFormat": "html", 3 | "commitList": "lazard@csail.mit.edu,nickolai@csail.mit.edu" 4 | } 5 | -------------------------------------------------------------------------------- /log/ansi/ansi_test.go: -------------------------------------------------------------------------------- 1 | package ansi 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestColors(t *testing.T) { 9 | for _, color := range AllColors { 10 | fmt.Printf("%s\t%s\n", Colorf(color, color), Colorf(color, color, Bold)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /encoding/toml/toml.go: -------------------------------------------------------------------------------- 1 | //go:generate -command yacc goyacc 2 | //go:generate yacc -o parser.go parser.y 3 | 4 | /* 5 | Package toml implements Tom's Obvious Minimal Language. 6 | 7 | This package implements a subset of the TOML specification that's useful 8 | for Alpenhorn config files. We built our own TOML package so that we 9 | could have control over how certain types are encoded. For example, 10 | []byte can be encoded as a base32 string. 11 | 12 | This package does not yet provide an encoder since most configs in Alpenhorn 13 | can be generated using a template. 14 | */ 15 | package toml 16 | -------------------------------------------------------------------------------- /cmd/cmdutil/util.go: -------------------------------------------------------------------------------- 1 | package cmdutil 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | func Overwrite(path string) bool { 10 | _, err := os.Stat(path) 11 | if os.IsNotExist(err) { 12 | return true 13 | } 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | fmt.Printf("%s already exists.\n", path) 18 | fmt.Printf("Overwrite (y/N)? ") 19 | var yesno [3]byte 20 | n, err := os.Stdin.Read(yesno[:]) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | if n == 0 { 25 | return false 26 | } 27 | if yesno[0] != 'y' && yesno[0] != 'Y' { 28 | return false 29 | } 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /log/output.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package log 6 | 7 | import ( 8 | "io" 9 | "os" 10 | "sync" 11 | ) 12 | 13 | var Stdout = NewMutexWriter(os.Stdout) 14 | 15 | var Stderr = NewMutexWriter(os.Stderr) 16 | 17 | type MutexWriter struct { 18 | mu sync.Mutex 19 | inner io.Writer 20 | } 21 | 22 | func NewMutexWriter(w io.Writer) *MutexWriter { 23 | return &MutexWriter{ 24 | inner: w, 25 | } 26 | } 27 | 28 | func (w *MutexWriter) Write(data []byte) (int, error) { 29 | w.mu.Lock() 30 | n, err := w.inner.Write(data) 31 | w.mu.Unlock() 32 | return n, err 33 | } 34 | -------------------------------------------------------------------------------- /pkg/errorcode_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ErrorCode"; DO NOT EDIT. 2 | 3 | package pkg 4 | 5 | import "fmt" 6 | 7 | const _ErrorCode_name = "ErrBadRequestJSONErrDatabaseErrorErrInvalidUsernameErrInvalidLoginKeyErrNotRegisteredErrAlreadyRegisteredErrRoundNotFoundErrInvalidUserLongTermKeyErrInvalidSignatureErrInvalidTokenErrExpiredTokenErrUnauthorizedErrBadCommitmentErrUnknown" 8 | 9 | var _ErrorCode_index = [...]uint8{0, 17, 33, 51, 69, 85, 105, 121, 146, 165, 180, 195, 210, 226, 236} 10 | 11 | func (i ErrorCode) String() string { 12 | i -= 1 13 | if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) { 14 | return fmt.Sprintf("ErrorCode(%d)", i+1) 15 | } 16 | return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /bootstrap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import "vuvuzela.io/alpenhorn/config" 8 | 9 | func (c *Client) Bootstrap(addFriendConfig, dialingConfig *config.SignedConfig) error { 10 | if err := addFriendConfig.Validate(); err != nil { 11 | return err 12 | } 13 | if err := dialingConfig.Validate(); err != nil { 14 | return err 15 | } 16 | 17 | c.mu.Lock() 18 | defer c.mu.Unlock() 19 | 20 | c.addFriendConfig = addFriendConfig 21 | c.addFriendConfigHash = addFriendConfig.Hash() 22 | 23 | c.dialingConfig = dialingConfig 24 | c.dialingConfigHash = dialingConfig.Hash() 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Alpenhorn: a system for bootstrapping private communication 2 | Copyright (C) 2016 David Lazar 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as 6 | published by the Free Software Foundation, either version 3 of the 7 | License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpenhorn 2 | 3 | Alpenhorn is the first system for initiating an encrypted connection 4 | between two users that provides strong privacy and forward secrecy 5 | guarantees for **metadata**. Alpenhorn does not require out-of-band 6 | communication other than knowing your friend's Alpenhorn username 7 | (usually their email address). Alpenhorn's design, threat model, and 8 | performance are described in our 9 | [OSDI 2016 paper](https://davidlazar.org/papers/alpenhorn.pdf). 10 | 11 | In short, Alpenhorn works well for bootstrapping conversations in 12 | [Vuvuzela](https://github.com/vuvuzela/vuvuzela). Now users can start 13 | chatting on Vuvuzela without having to exchange keys in person or over 14 | some less secure channel. 15 | 16 | A beta deployment of Alpenhorn and Vuvuzela is coming soon. 17 | -------------------------------------------------------------------------------- /encoding/toml/lexer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package toml 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | var lexData = `# hello world 12 | # foobar 13 | [hello.world]x = [1,-22.987,3,true,[false]] 14 | y = false 15 | 16 | [[thing.fruit]] 17 | name = "apple" 18 | ` 19 | 20 | func TestLex(t *testing.T) { 21 | lx := lex("test", lexData, lexTableBody) 22 | vals := []string{"[", "hello", "world", "]", "x", "=", "[", "1", ",", "-22.987", ",", "3", ",", "true", ",", "[", "false", "]", "]", "y", "=", "false", "[[", "thing", "fruit", "]]", "name", "=", "\"apple\""} 23 | i := 0 24 | for { 25 | item := lx.nextItem() 26 | if item.typ == eof || item.typ == itemError { 27 | break 28 | } 29 | if item.val != vals[i] { 30 | t.Fatalf("item %d: got %q want %q", i, item.val, vals[i]) 31 | } 32 | i++ 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/pg/pg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pg 6 | 7 | import ( 8 | "database/sql" 9 | "fmt" 10 | "log" 11 | ) 12 | 13 | func Createdb(dbname string) { 14 | connstr := "host=/var/run/postgresql dbname=postgres" 15 | db, err := sql.Open("postgres", connstr) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | defer db.Close() 20 | 21 | _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s;", dbname)) 22 | if err != nil { 23 | log.Fatalf("dropdb: %s", err) 24 | } 25 | _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s;", dbname)) 26 | if err != nil { 27 | log.Fatalf("createdb: %s", err) 28 | } 29 | } 30 | 31 | func Dropdb(dbname string) { 32 | connstr := "host=/var/run/postgresql dbname=postgres" 33 | db, err := sql.Open("postgres", connstr) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | defer db.Close() 38 | 39 | _, err = db.Exec(fmt.Sprintf("DROP DATABASE %s;", dbname)) 40 | if err != nil { 41 | log.Fatalf("dropdb: %s", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/data_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "crypto/rand" 10 | "reflect" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestMarshalUserState(t *testing.T) { 16 | publicKey, _, _ := ed25519.GenerateKey(rand.Reader) 17 | 18 | verifiedUser := userState{ 19 | LoginKey: publicKey, 20 | } 21 | data := verifiedUser.Marshal() 22 | 23 | var verifiedUser2 userState 24 | if err := verifiedUser2.Unmarshal(data); err != nil { 25 | t.Fatal(err) 26 | } 27 | if !reflect.DeepEqual(verifiedUser, verifiedUser2) { 28 | t.Fatalf("got %#v, want %#v", verifiedUser2, verifiedUser) 29 | } 30 | } 31 | 32 | func TestMarshalLastExtraction(t *testing.T) { 33 | e := lastExtraction{ 34 | Round: 12345, 35 | UnixTime: time.Now().Unix(), 36 | } 37 | data := e.Marshal() 38 | var e2 lastExtraction 39 | if err := e2.Unmarshal(data); err != nil { 40 | t.Fatal(err) 41 | } 42 | if !reflect.DeepEqual(e, e2) { 43 | t.Fatalf("after unmarshal: got %#v, want %#v", e2, e) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /edtls/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 Tommi Virtanen. 2 | Portions Copyright (c) 2016 David Lazar. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /pkg/extract_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "crypto/rand" 10 | "encoding/json" 11 | "reflect" 12 | "testing" 13 | 14 | "vuvuzela.io/crypto/bls" 15 | ) 16 | 17 | func TestMarshalExtractReply(t *testing.T) { 18 | _, serverPriv, _ := ed25519.GenerateKey(rand.Reader) 19 | ctxt := make([]byte, 128) 20 | rand.Read(ctxt) 21 | 22 | _, blsPriv, _ := bls.GenerateKey(rand.Reader) 23 | sig := bls.Sign(blsPriv, []byte("test message")) 24 | 25 | reply := &extractReply{ 26 | Round: 12345, 27 | Username: "alice@example.org", 28 | EncryptedPrivateKey: ctxt, 29 | IdentitySig: sig, 30 | } 31 | reply.Sign(serverPriv) 32 | data, err := json.Marshal(reply) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | ureply := new(extractReply) 38 | if err := json.Unmarshal(data, ureply); err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if !reflect.DeepEqual(reply, ureply) { 43 | t.Fatalf("after unmarshal: got %#v, want %#v", ureply, reply) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/register_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "crypto/rand" 10 | "fmt" 11 | "io/ioutil" 12 | "os" 13 | "testing" 14 | 15 | "vuvuzela.io/alpenhorn/log" 16 | ) 17 | 18 | func BenchmarkRegister(b *testing.B) { 19 | _, serverPriv, _ := ed25519.GenerateKey(rand.Reader) 20 | dbPath, err := ioutil.TempDir("", "alpenhorn_pkg_db_") 21 | if err != nil { 22 | b.Fatal(err) 23 | } 24 | defer os.RemoveAll(dbPath) 25 | 26 | conf := &Config{ 27 | DBPath: dbPath, 28 | Logger: &log.Logger{ 29 | Level: log.ErrorLevel, 30 | EntryHandler: &log.OutputText{Out: log.Stderr}, 31 | }, 32 | SigningKey: serverPriv, 33 | } 34 | 35 | srv, err := NewServer(conf) 36 | if err != nil { 37 | b.Fatal(err) 38 | } 39 | defer srv.Close() 40 | 41 | userPub, _, _ := ed25519.GenerateKey(rand.Reader) 42 | b.ResetTimer() 43 | 44 | for i := 0; i < b.N; i++ { 45 | args := ®isterArgs{ 46 | Username: fmt.Sprintf("%dbenchmark", i), 47 | LoginKey: userPub, 48 | } 49 | err = srv.register(args) 50 | if err != nil { 51 | b.Fatal(err) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mailbox.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "crypto/sha256" 9 | "encoding/binary" 10 | "fmt" 11 | "io/ioutil" 12 | "net/url" 13 | 14 | "vuvuzela.io/alpenhorn/config" 15 | "vuvuzela.io/alpenhorn/errors" 16 | ) 17 | 18 | func (c *Client) fetchMailbox(cdnConfig config.CDNServerConfig, baseURL string, mailboxID uint32) ([]byte, error) { 19 | u, err := url.Parse(baseURL) 20 | if err != nil { 21 | return nil, errors.Wrap(err, "parsing mailbox url") 22 | } 23 | vals := u.Query() 24 | vals.Set("key", fmt.Sprintf("%d", mailboxID)) 25 | u.RawQuery = vals.Encode() 26 | 27 | resp, err := c.edhttpClient.Get(cdnConfig.Key, u.String()) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer resp.Body.Close() 32 | 33 | mailbox, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "reading mailbox body") 36 | } 37 | return mailbox, nil 38 | } 39 | 40 | func usernameToMailbox(username string, numMailboxes uint32) uint32 { 41 | h := sha256.Sum256([]byte(username)) 42 | k := binary.BigEndian.Uint32(h[0:4]) 43 | mbox := k%numMailboxes + 1 44 | // do this check at the end to minimize timing leak 45 | if username == "" { 46 | return 0 47 | } else { 48 | return mbox 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /edtls/client_test.go: -------------------------------------------------------------------------------- 1 | package edtls 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "testing" 10 | ) 11 | 12 | func TestClientVerificationFailure(t *testing.T) { 13 | _, serverKeyPriv, err := ed25519.GenerateKey(rand.Reader) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | pipe := localPipe() 19 | defer pipe.Close() 20 | 21 | go func() { 22 | c := Server(pipe.server, serverKeyPriv) 23 | _, _ = io.Copy(ioutil.Discard, c) 24 | }() 25 | 26 | _, clientKey, _ := ed25519.GenerateKey(rand.Reader) 27 | otherPub, _, _ := ed25519.GenerateKey(rand.Reader) 28 | 29 | c := Client(pipe.client, otherPub, clientKey) 30 | err = c.Handshake() 31 | if err != ErrVerificationFailed { 32 | t.Fatalf("expected ErrVerificationFailed, got %T: %v", err, err) 33 | } 34 | } 35 | 36 | type pipe struct { 37 | listener net.Listener 38 | server net.Conn 39 | client net.Conn 40 | } 41 | 42 | func (p pipe) Close() { 43 | p.client.Close() 44 | p.server.Close() 45 | p.listener.Close() 46 | } 47 | 48 | func localPipe() pipe { 49 | l, err := net.Listen("tcp", "127.0.0.1:0") 50 | if err != nil { 51 | panic(err) 52 | } 53 | addr := l.Addr() 54 | c, err := net.Dial(addr.Network(), addr.String()) 55 | if err != nil { 56 | panic(err) 57 | } 58 | s, err := l.Accept() 59 | if err != nil { 60 | panic(err) 61 | } 62 | return pipe{ 63 | listener: l, 64 | client: c, 65 | server: s, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package errors implements basic error handling. 6 | // 7 | // This package is like github.com/pkg/errors but without the stack traces. 8 | package errors 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | type errorString struct { 15 | msg string 16 | } 17 | 18 | func (e *errorString) Error() string { 19 | return e.msg 20 | } 21 | 22 | func New(format string, a ...interface{}) error { 23 | return &errorString{fmt.Sprintf(format, a...)} 24 | } 25 | 26 | type withCause struct { 27 | cause error 28 | msg string 29 | } 30 | 31 | func (e *withCause) Error() string { 32 | return e.msg + ": " + e.cause.Error() 33 | } 34 | 35 | func (e *withCause) Cause() error { 36 | return e.cause 37 | } 38 | 39 | func Wrap(err error, format string, a ...interface{}) error { 40 | return &withCause{ 41 | cause: err, 42 | msg: fmt.Sprintf(format, a...), 43 | } 44 | } 45 | 46 | type causer interface { 47 | Cause() error 48 | } 49 | 50 | // Cause returns the first cause of the error or returns the original error 51 | // if the error does not have a cause. This is unlike the pkg/errors package 52 | // which returns the most underlying cause for the error. 53 | func Cause(err error) error { 54 | cause, ok := err.(causer) 55 | if !ok { 56 | return err 57 | } 58 | return cause.Cause() 59 | } 60 | -------------------------------------------------------------------------------- /typesocket/mux.go: -------------------------------------------------------------------------------- 1 | package typesocket 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | ) 7 | 8 | type Mux map[string]*muxEntry 9 | 10 | type muxEntry struct { 11 | fn reflect.Value 12 | argType reflect.Type 13 | } 14 | 15 | // NewMux creates a new mux from the given handlers. 16 | // The key in the handlers map is a message ID and the 17 | // interface{} value must be of type func(Conn, T) for 18 | // some type T. 19 | func NewMux(handlers map[string]interface{}) Mux { 20 | mux := make(map[string]*muxEntry) 21 | for k, fn := range handlers { 22 | ty := reflect.TypeOf(fn) 23 | mux[k] = &muxEntry{ 24 | fn: reflect.ValueOf(fn), 25 | argType: ty.In(1), 26 | } 27 | } 28 | return Mux(mux) 29 | } 30 | 31 | type envelope struct { 32 | ID string 33 | Message json.RawMessage 34 | } 35 | 36 | func encodeMessage(msgID string, v interface{}) ([]byte, error) { 37 | rawMsg, err := json.Marshal(v) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | e := &envelope{ 43 | ID: msgID, 44 | Message: rawMsg, 45 | } 46 | msgBytes, err := json.Marshal(e) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return msgBytes, nil 51 | } 52 | 53 | func (m Mux) openEnvelope(conn Conn, e *envelope) { 54 | h := m[e.ID] 55 | if h == nil { 56 | return 57 | } 58 | 59 | arg := reflect.New(h.argType) 60 | if err := json.Unmarshal(e.Message, arg.Interface()); err != nil { 61 | return 62 | } 63 | 64 | h.fn.Call([]reflect.Value{reflect.ValueOf(conn), arg.Elem()}) 65 | } 66 | -------------------------------------------------------------------------------- /cmd/guardian/alpenhorn-guardian-upload-config/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | 15 | "vuvuzela.io/alpenhorn/config" 16 | // Register the convo inner config. 17 | _ "vuvuzela.io/vuvuzela/convo" 18 | ) 19 | 20 | var configPath = flag.String("config", "", "path to new signed config") 21 | var configServerURL = flag.String("url", "", "url of config server") 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | if *configPath == "" { 27 | fmt.Println("Specify config file with -config.") 28 | os.Exit(1) 29 | } 30 | 31 | configBytes, err := ioutil.ReadFile(*configPath) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | conf := new(config.SignedConfig) 36 | 37 | if err := json.Unmarshal(configBytes, conf); err != nil { 38 | log.Fatalf("error decoding json: %s", err) 39 | } 40 | if err := conf.Validate(); err != nil { 41 | log.Fatalf("invalid config: %s", err) 42 | } 43 | 44 | var client *config.Client 45 | if *configServerURL == "" { 46 | client = config.StdClient 47 | } else { 48 | client = &config.Client{ 49 | ConfigServerURL: *configServerURL, 50 | } 51 | } 52 | err = client.SetCurrentConfig(conf) 53 | if err != nil { 54 | log.Fatalf("failed to set config: %s", err) 55 | } 56 | 57 | fmt.Printf("Success: uploaded config with hash %s\n", conf.Hash()) 58 | } 59 | -------------------------------------------------------------------------------- /log/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 David Lazar. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name Alpenhorn nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /log/global.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package log 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/mattn/go-isatty" 11 | ) 12 | 13 | var StdLogger = &Logger{ 14 | EntryHandler: &OutputText{ 15 | Out: Stderr, 16 | DisableColors: !isatty.IsTerminal(os.Stderr.Fd()), 17 | }, 18 | Level: InfoLevel, 19 | } 20 | 21 | func WithFields(fields Fields) *Logger { return StdLogger.WithFields(fields) } 22 | func Info(args ...interface{}) { StdLogger.Info(args...) } 23 | func Infof(format string, args ...interface{}) { StdLogger.Infof(format, args...) } 24 | func Error(args ...interface{}) { StdLogger.Error(args...) } 25 | func Errorf(format string, args ...interface{}) { StdLogger.Errorf(format, args...) } 26 | func Warn(args ...interface{}) { StdLogger.Warn(args...) } 27 | func Warnf(format string, args ...interface{}) { StdLogger.Warnf(format, args...) } 28 | func Fatal(args ...interface{}) { StdLogger.Fatal(args...) } 29 | func Fatalf(format string, args ...interface{}) { StdLogger.Fatalf(format, args...) } 30 | func Debug(args ...interface{}) { StdLogger.Debug(args...) } 31 | func Debugf(format string, args ...interface{}) { StdLogger.Debugf(format, args...) } 32 | func Panic(args ...interface{}) { StdLogger.Panic(args...) } 33 | func Panicf(format string, args ...interface{}) { StdLogger.Panicf(format, args...) } 34 | -------------------------------------------------------------------------------- /bloom/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 David Lazar. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name Alpenhorn nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /errors/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 David Lazar. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name Alpenhorn nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /encoding/toml/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 David Lazar. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name Alpenhorn nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /log/levels.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package log 6 | 7 | import "vuvuzela.io/alpenhorn/log/ansi" 8 | 9 | // Level is a logging level. The levels are copied from logrus. 10 | type Level uint32 11 | 12 | const ( 13 | PanicLevel Level = iota 14 | FatalLevel 15 | ErrorLevel 16 | WarnLevel 17 | InfoLevel 18 | DebugLevel 19 | ) 20 | 21 | func (level Level) String() string { 22 | switch level { 23 | case DebugLevel: 24 | return "debug" 25 | case InfoLevel: 26 | return "info" 27 | case WarnLevel: 28 | return "warning" 29 | case ErrorLevel: 30 | return "error" 31 | case FatalLevel: 32 | return "fatal" 33 | case PanicLevel: 34 | return "panic" 35 | } 36 | 37 | return "unknown" 38 | } 39 | 40 | func (level Level) Icon() string { 41 | switch level { 42 | case DebugLevel: 43 | return "·" 44 | case InfoLevel: 45 | return " " 46 | case WarnLevel: 47 | return "~" 48 | case ErrorLevel: 49 | return "!" 50 | case FatalLevel: 51 | return "*" 52 | case PanicLevel: 53 | return "X" 54 | } 55 | 56 | return "UNKNOWN" 57 | } 58 | 59 | func (level Level) Color() ansi.Code { 60 | switch level { 61 | case DebugLevel: 62 | return ansi.White 63 | case InfoLevel: 64 | return ansi.Cyan 65 | case WarnLevel: 66 | return ansi.Yellow 67 | case ErrorLevel: 68 | return ansi.Red 69 | case FatalLevel: 70 | return ansi.Red 71 | case PanicLevel: 72 | return ansi.Red 73 | default: 74 | return ansi.Red 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cmd/guardian/alpenhorn-guardian-new-config/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "log" 12 | "os" 13 | "time" 14 | 15 | "vuvuzela.io/alpenhorn/config" 16 | // Register the convo inner config. 17 | _ "vuvuzela.io/vuvuzela/convo" 18 | ) 19 | 20 | var service = flag.String("service", "", "service name") 21 | var printCurrent = flag.Bool("current", false, "print current config") 22 | var configServerURL = flag.String("url", "", "url of config server") 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if *service == "" { 28 | fmt.Println("Specify a service name with -service.") 29 | os.Exit(1) 30 | } 31 | 32 | var client *config.Client 33 | if *configServerURL == "" { 34 | client = config.StdClient 35 | } else { 36 | client = &config.Client{ 37 | ConfigServerURL: *configServerURL, 38 | } 39 | } 40 | 41 | conf, err := client.CurrentConfig(*service) 42 | if err != nil { 43 | log.Fatalf("failed to fetch current config: %s", err) 44 | } 45 | confHash := conf.Hash() 46 | 47 | if !*printCurrent { 48 | conf.Inner.UseLatestVersion() 49 | valid := conf.Expires.Sub(conf.Created) 50 | conf.Created = time.Now() 51 | conf.Expires = conf.Created.Add(valid) 52 | conf.PrevConfigHash = confHash 53 | conf.Signatures = make(map[string][]byte) 54 | } 55 | 56 | data, err := json.MarshalIndent(conf, "", " ") 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | fmt.Printf("%s\n", data) 62 | } 63 | -------------------------------------------------------------------------------- /coordinator/persist.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package coordinator 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | 13 | "vuvuzela.io/internal/ioutil2" 14 | ) 15 | 16 | // version is the current version number of the persisted state format. 17 | const version byte = 1 18 | 19 | type persistedState struct { 20 | Round uint32 21 | } 22 | 23 | func (srv *Server) LoadPersistedState() error { 24 | data, err := ioutil.ReadFile(srv.PersistPath) 25 | if err != nil { 26 | return err 27 | } 28 | if len(data) == 0 { 29 | return fmt.Errorf("no data: %s", srv.PersistPath) 30 | } 31 | 32 | ver := data[0] 33 | if ver != version { 34 | return fmt.Errorf("unexpected version: want version %d, got %d", version, ver) 35 | } 36 | 37 | var st persistedState 38 | err = json.Unmarshal(data[1:], &st) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | srv.mu.Lock() 44 | srv.round = st.Round 45 | srv.mu.Unlock() 46 | 47 | return nil 48 | } 49 | 50 | func (srv *Server) Persist() error { 51 | srv.mu.Lock() 52 | err := srv.persistLocked() 53 | srv.mu.Unlock() 54 | return err 55 | } 56 | 57 | func (srv *Server) persistLocked() error { 58 | st := &persistedState{ 59 | Round: srv.round, 60 | } 61 | 62 | buf := new(bytes.Buffer) 63 | buf.WriteByte(version) 64 | enc := json.NewEncoder(buf) 65 | enc.SetIndent("", " ") // for easier debugging 66 | err := enc.Encode(st) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return ioutil2.WriteFileAtomic(srv.PersistPath, buf.Bytes(), 0600) 72 | } 73 | -------------------------------------------------------------------------------- /cmd/guardian/alpenhorn-guardian-send-announcement/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | 16 | "vuvuzela.io/alpenhorn/cmd/guardian" 17 | "vuvuzela.io/alpenhorn/config" 18 | "vuvuzela.io/alpenhorn/edhttp" 19 | "vuvuzela.io/vuvuzela/convo" 20 | "vuvuzela.io/vuvuzela/coordinator" 21 | ) 22 | 23 | var globalMsg = flag.String("msg", "", "message to announce") 24 | 25 | func main() { 26 | flag.Parse() 27 | 28 | if *globalMsg == "" { 29 | fmt.Println("Specify message with -msg.") 30 | os.Exit(1) 31 | } 32 | 33 | conf, err := config.StdClient.CurrentConfig("Convo") 34 | if err != nil { 35 | fmt.Printf("error fetching convo config: %s\n", err) 36 | os.Exit(1) 37 | } 38 | convoConfig := conf.Inner.(*convo.ConvoConfig) 39 | 40 | appDir := guardian.Appdir() 41 | privatePath := filepath.Join(appDir, "guardian.privatekey") 42 | 43 | privateKey := guardian.ReadPrivateKey(privatePath) 44 | 45 | url := fmt.Sprintf("https://%s/convo/sendannouncement", convoConfig.Coordinator.Address) 46 | client := edhttp.Client{ 47 | Key: privateKey, 48 | } 49 | resp, err := client.PostJSON(convoConfig.Coordinator.Key, url, coordinator.GlobalAnnouncement{ 50 | Message: *globalMsg, 51 | }) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | defer resp.Body.Close() 56 | reply, _ := ioutil.ReadAll(resp.Body) 57 | if resp.StatusCode != http.StatusOK { 58 | log.Fatalf("%s: %s", resp.Status, reply) 59 | } 60 | log.Printf("%s: %s", resp.Status, reply) 61 | } 62 | -------------------------------------------------------------------------------- /typesocket/typesocket_test.go: -------------------------------------------------------------------------------- 1 | package typesocket 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "testing" 10 | "time" 11 | 12 | "vuvuzela.io/alpenhorn/edtls" 13 | ) 14 | 15 | type Ping struct { 16 | Count int 17 | } 18 | 19 | func TestTypeSocket(t *testing.T) { 20 | serverPublic, serverPrivate, _ := ed25519.GenerateKey(rand.Reader) 21 | 22 | serverMux := NewMux(map[string]interface{}{ 23 | "Ping": func(c Conn, p Ping) { 24 | log.Printf("server: ping %d -> %d", p.Count, p.Count+1) 25 | if err := c.Send("Ping", Ping{p.Count + 1}); err != nil { 26 | t.Fatal(err) 27 | } 28 | }, 29 | }) 30 | hub := &Hub{ 31 | Mux: serverMux, 32 | conns: make(map[*serverConn]bool), 33 | } 34 | httpMux := http.NewServeMux() 35 | httpMux.Handle("/ws", hub) 36 | l, err := edtls.Listen("tcp", "127.0.0.1:0", serverPrivate) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | defer l.Close() 41 | go http.Serve(l, httpMux) 42 | 43 | done := make(chan struct{}) 44 | clientMux := NewMux(map[string]interface{}{ 45 | "Ping": func(c Conn, p Ping) { 46 | if p.Count > 10 { 47 | close(done) 48 | log.Printf("client done: %d", p.Count) 49 | } else { 50 | log.Printf("client: ping %d -> %d", p.Count, p.Count+1) 51 | if err := c.Send("Ping", Ping{p.Count + 1}); err != nil { 52 | t.Fatal(err) 53 | } 54 | } 55 | }, 56 | }) 57 | 58 | time.Sleep(500 * time.Millisecond) 59 | conn, err := Dial(fmt.Sprintf("wss://%s/ws", l.Addr().String()), serverPublic) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | go conn.Serve(clientMux) 64 | defer conn.Close() 65 | if err := conn.Send("Ping", Ping{0}); err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | select { 70 | case <-done: 71 | case <-time.After(1 * time.Second): 72 | t.Fatal("timeout") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /edtls/doc.go: -------------------------------------------------------------------------------- 1 | // Package edtls provides ed25519 signatures on top of TLS certificates. 2 | // 3 | // There is currently no standard way to use ed25519 in TLS. See drafts at 4 | // http://ietfreport.isoc.org/idref/draft-josefsson-eddsa-ed25519/ 5 | // for standardization attempts. 6 | // 7 | // The way the TLS protocol is designed, it relies on centralized 8 | // registries of algorithms. We cannot easily plug in a new kind of a 9 | // certificate. Instead, we abuse the extension mechanism to transmit 10 | // an extra, custom, certificate. 11 | // 12 | // Clients connecting to servers are expected to already know the 13 | // ed25519 public key of the server. Clients will announce their 14 | // public key, and the server-side logic can use that for 15 | // authentication and access control. 16 | // 17 | // In both directions a "vouch" is transmitted as a TLS extension. It 18 | // contains an ed25519 public key and a signature of the certificate 19 | // expiry time and the DER-encoded TLS public key. 20 | // 21 | // If a vouch packet opens without errors, and contents match the TLS 22 | // public key of the sender, the receiver knows that the sender 23 | // actually owns the ed25519 public key and the TLS public key. 24 | // 25 | // Vouches cryptographically verify the expiry time of the TLS 26 | // certificate, to make sure that an attacker did not manage to just 27 | // steal the TLS private key, but also holds the ed25519 private key. 28 | // As the TLS private key lives in the same memory space as the 29 | // ed25519 private keys, an attack may be able to steal both, but 30 | // off-the-shelf attacks will typically only target the TLS key. 31 | // 32 | // There is currently no mechanism to rotate the ed25519 keys. 33 | // 34 | // This package is a fork of https://github.com/bazil/bazil/tree/7d1f80b3/util/edtls. 35 | // This fork uses the new ed25519 package, adds the Dial, Listen, and 36 | // Server methods, rotates TLS server certificates, hides the tls.Config 37 | // parameters, and more. 38 | package edtls 39 | -------------------------------------------------------------------------------- /intro.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "encoding/binary" 11 | "unsafe" 12 | 13 | "vuvuzela.io/alpenhorn/pkg" 14 | "vuvuzela.io/crypto/bls" 15 | ) 16 | 17 | const ( 18 | sizeIntro = int(unsafe.Sizeof(introduction{})) 19 | ) 20 | 21 | type introduction struct { 22 | Username [64]byte 23 | DHPublicKey [32]byte 24 | DialingRound uint32 25 | LongTermKey [32]byte 26 | Signature [64]byte 27 | ServerMultisig [32]byte 28 | } 29 | 30 | func (i *introduction) MarshalBinary() ([]byte, error) { 31 | buf := new(bytes.Buffer) 32 | if err := binary.Write(buf, binary.BigEndian, i); err != nil { 33 | return nil, err 34 | } 35 | return buf.Bytes(), nil 36 | } 37 | 38 | func (i *introduction) UnmarshalBinary(data []byte) error { 39 | buf := bytes.NewReader(data) 40 | return binary.Read(buf, binary.BigEndian, i) 41 | } 42 | 43 | func (i *introduction) Verify(serverKeys []*bls.PublicKey) bool { 44 | longTermKey := ed25519.PublicKey(i.LongTermKey[:]) 45 | 46 | msgs := make([][]byte, len(serverKeys)) 47 | for j, key := range serverKeys { 48 | attestation := &pkg.Attestation{ 49 | AttestKey: key, 50 | UserIdentity: &i.Username, 51 | UserLongTermKey: longTermKey, 52 | } 53 | msgs[j] = attestation.Marshal() 54 | } 55 | ok1 := bls.VerifyCompressed(serverKeys, msgs, &i.ServerMultisig) 56 | 57 | ok2 := ed25519.Verify(longTermKey, i.msg(), i.Signature[:]) 58 | 59 | return ok1 && ok2 60 | } 61 | 62 | func (i *introduction) Sign(key ed25519.PrivateKey) { 63 | sig := ed25519.Sign(key, i.msg()) 64 | copy(i.Signature[:], sig) 65 | } 66 | 67 | func (i *introduction) msg() []byte { 68 | buf := new(bytes.Buffer) 69 | buf.WriteString("Introduction") 70 | buf.Write(i.Username[:]) 71 | buf.Write(i.DHPublicKey[:]) 72 | binary.Write(buf, binary.BigEndian, i.DialingRound) 73 | return buf.Bytes() 74 | } 75 | -------------------------------------------------------------------------------- /cmd/guardian/alpenhorn-guardian-sign-config/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "encoding/json" 11 | "flag" 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | 17 | "github.com/davidlazar/go-crypto/encoding/base32" 18 | 19 | "vuvuzela.io/alpenhorn/cmd/guardian" 20 | "vuvuzela.io/alpenhorn/config" 21 | "vuvuzela.io/alpenhorn/log" 22 | 23 | // Register the convo inner config. 24 | _ "vuvuzela.io/vuvuzela/convo" 25 | ) 26 | 27 | var configPath = flag.String("config", "", "path to new signed config") 28 | 29 | func main() { 30 | flag.Parse() 31 | 32 | if *configPath == "" { 33 | fmt.Println("Specify config file with -config.") 34 | os.Exit(1) 35 | } 36 | 37 | configBytes, err := ioutil.ReadFile(*configPath) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | conf := new(config.SignedConfig) 42 | 43 | if err := json.Unmarshal(configBytes, conf); err != nil { 44 | log.Fatalf("error decoding json: %s", err) 45 | } 46 | if err := conf.Validate(); err != nil { 47 | log.Fatalf("invalid config: %s", err) 48 | } 49 | 50 | appDir := guardian.Appdir() 51 | privatePath := filepath.Join(appDir, "guardian.privatekey") 52 | 53 | privateKey := guardian.ReadPrivateKey(privatePath) 54 | publicKey := privateKey.Public().(ed25519.PublicKey) 55 | 56 | myPos := -1 57 | for i, g := range conf.Guardians { 58 | if bytes.Equal(g.Key, publicKey) { 59 | myPos = i 60 | } 61 | } 62 | if myPos == -1 { 63 | fmt.Fprintf(os.Stderr, "! Warning: your key is not in the supplied config's Guardian list!\n") 64 | } 65 | 66 | msg := conf.SigningMessage() 67 | sig := ed25519.Sign(privateKey, msg) 68 | if conf.Signatures == nil { 69 | conf.Signatures = make(map[string][]byte) 70 | } 71 | conf.Signatures[base32.EncodeToString(publicKey)] = sig 72 | 73 | data, err := json.MarshalIndent(conf, "", " ") 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | fmt.Printf("%s\n", data) 79 | } 80 | -------------------------------------------------------------------------------- /cmd/guardian/guardian.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package guardian 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "os/user" 14 | "path/filepath" 15 | "strings" 16 | 17 | "github.com/davidlazar/go-crypto/encoding/base32" 18 | "golang.org/x/crypto/nacl/secretbox" 19 | "golang.org/x/crypto/scrypt" 20 | "golang.org/x/crypto/ssh/terminal" 21 | ) 22 | 23 | func Appdir() string { 24 | u, err := user.Current() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | return filepath.Join(u.HomeDir, ".alpenhorn") 29 | } 30 | 31 | func DeriveKey(passphrase []byte) []byte { 32 | dk, err := scrypt.Key(passphrase, []byte("alpenhorn-guardian"), 2<<15, 8, 1, 32) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return dk 37 | } 38 | 39 | const nonceOverhead = 24 40 | 41 | func ReadPrivateKey(path string) ed25519.PrivateKey { 42 | data, err := ioutil.ReadFile(path) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | bs, err := base32.DecodeString(strings.TrimSpace(string(data))) 48 | if err != nil { 49 | log.Fatalf("error decoding base32: %s: %s", path, err) 50 | } 51 | 52 | expectedSize := nonceOverhead + ed25519.PrivateKeySize + secretbox.Overhead 53 | if len(bs) != expectedSize { 54 | log.Fatalf("unexpected key length: got %d bytes, want %d", len(bs), expectedSize) 55 | } 56 | 57 | var nonce [24]byte 58 | copy(nonce[:], bs[0:24]) 59 | ctxt := bs[24:] 60 | 61 | for { 62 | fmt.Fprintf(os.Stderr, "Enter passphrase for guardian key: ") 63 | pw, err := terminal.ReadPassword(0) 64 | fmt.Fprintln(os.Stderr) 65 | if err != nil { 66 | log.Fatalf("terminal.ReadPassword: %s", err) 67 | } 68 | 69 | dk := DeriveKey(pw) 70 | var boxKey [32]byte 71 | copy(boxKey[:], dk) 72 | 73 | msg, ok := secretbox.Open(nil, ctxt, &nonce, &boxKey) 74 | if ok { 75 | privateKey := ed25519.PrivateKey(msg) 76 | return privateKey 77 | } 78 | fmt.Fprintln(os.Stderr, "Wrong passphrase. Try again.") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /config/persist.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "io/ioutil" 11 | 12 | "vuvuzela.io/alpenhorn/errors" 13 | "vuvuzela.io/internal/ioutil2" 14 | ) 15 | 16 | type persistedState struct { 17 | AllConfigs map[string]*SignedConfig 18 | CurrentConfig map[string]string 19 | } 20 | 21 | const persistVersion byte = 1 22 | 23 | func writeState(path string, state *persistedState) error { 24 | buf := new(bytes.Buffer) 25 | buf.WriteByte(persistVersion) 26 | enc := json.NewEncoder(buf) 27 | enc.SetIndent("", " ") 28 | err := enc.Encode(state) 29 | if err != nil { 30 | return errors.Wrap(err, "json.Encode") 31 | } 32 | 33 | return ioutil2.WriteFileAtomic(path, buf.Bytes(), 0600) 34 | } 35 | 36 | func (srv *Server) persistLocked() error { 37 | state := &persistedState{ 38 | AllConfigs: srv.allConfigs, 39 | CurrentConfig: srv.currentConfig, 40 | } 41 | return writeState(srv.persistPath, state) 42 | } 43 | 44 | func LoadServer(persistPath string) (*Server, error) { 45 | data, err := ioutil.ReadFile(persistPath) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if data[0] != persistVersion { 50 | return nil, errors.New("unknown state version: got %d, want %d", data[0], persistVersion) 51 | } 52 | var state persistedState 53 | err = json.Unmarshal(data[1:], &state) 54 | if err != nil { 55 | return nil, errors.Wrap(err, "json.Unmarshal") 56 | } 57 | 58 | for service, hash := range state.CurrentConfig { 59 | _, ok := state.AllConfigs[hash] 60 | if !ok { 61 | return nil, errors.New("current %q config (%q) not found in persisted state", service, hash) 62 | } 63 | } 64 | 65 | return &Server{ 66 | persistPath: persistPath, 67 | 68 | allConfigs: state.AllConfigs, 69 | currentConfig: state.CurrentConfig, 70 | }, nil 71 | } 72 | 73 | func CreateServer(persistPath string) (*Server, error) { 74 | server := &Server{ 75 | persistPath: persistPath, 76 | allConfigs: make(map[string]*SignedConfig), 77 | currentConfig: make(map[string]string), 78 | } 79 | err := server.persistLocked() 80 | return server, err 81 | } 82 | -------------------------------------------------------------------------------- /encoding/toml/decode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package toml 6 | 7 | import ( 8 | "reflect" 9 | "time" 10 | 11 | "github.com/davidlazar/go-crypto/encoding/base32" 12 | "github.com/davidlazar/mapstructure" 13 | ) 14 | 15 | // Unmarshal parses the TOML-encoded data and stores the result in the 16 | // value pointed to by v. Unmarshal has special cases for the following 17 | // types: 18 | // 19 | // []byte can be encoded as a base32 string 20 | // time.Duration can be encoded as a string in the form "72h3m0.5s" 21 | // 22 | func Unmarshal(data []byte, v interface{}) error { 23 | m, err := parse(string(data)) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | hook := mapstructure.ComposeDecodeHookFunc( 29 | stringToBytesHook, 30 | stringToTimeHook, 31 | mapstructure.StringToTimeDurationHookFunc(), 32 | ) 33 | 34 | decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 35 | DecodeHook: hook, 36 | Result: v, 37 | }) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return decoder.Decode(m) 43 | } 44 | 45 | func EncodeBytes(data []byte) string { 46 | return base32.EncodeToString(data) 47 | } 48 | 49 | func DecodeBytes(str string) ([]byte, error) { 50 | return base32.DecodeString(str) 51 | } 52 | 53 | func stringToBytesHook(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { 54 | if from.Kind() != reflect.String { 55 | return data, nil 56 | } 57 | if !to.AssignableTo(reflect.TypeOf([]byte{})) { 58 | return data, nil 59 | } 60 | return DecodeBytes(data.(string)) 61 | } 62 | 63 | func stringToTimeHook(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) { 64 | if from.Kind() != reflect.String { 65 | return data, nil 66 | } 67 | if to != reflect.TypeOf(time.Time{}) { 68 | return data, nil 69 | } 70 | 71 | return time.Parse(time.RFC3339, data.(string)) 72 | } 73 | 74 | func parse(str string) (map[string]interface{}, error) { 75 | // TODO lex name 76 | lx := lex("test", str, lexTableBody) 77 | r := yyParse(lx) 78 | if r == 0 || lx.err == nil { 79 | return lx.result, lx.err 80 | } 81 | return nil, lx.err 82 | } 83 | -------------------------------------------------------------------------------- /edtls/client.go: -------------------------------------------------------------------------------- 1 | package edtls 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ed25519" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "net" 9 | 10 | "vuvuzela.io/alpenhorn/errors" 11 | ) 12 | 13 | var ( 14 | ErrNoPeerCertificates = errors.New("peer did not supply a certificate") 15 | ErrVerificationFailed = errors.New("failed to verify certificate") 16 | ) 17 | 18 | func Dial(network, addr string, theirKey ed25519.PublicKey, myKey ed25519.PrivateKey) (*tls.Conn, error) { 19 | config := NewTLSClientConfig(myKey, theirKey) 20 | 21 | return tls.Dial(network, addr, config) 22 | } 23 | 24 | func Client(rawConn net.Conn, theirKey ed25519.PublicKey, myKey ed25519.PrivateKey) *tls.Conn { 25 | config := NewTLSClientConfig(myKey, theirKey) 26 | 27 | conn := tls.Client(rawConn, config) 28 | return conn 29 | } 30 | 31 | func NewTLSClientConfig(myKey ed25519.PrivateKey, peerKey ed25519.PublicKey) *tls.Config { 32 | var config = &tls.Config{ 33 | RootCAs: x509.NewCertPool(), 34 | ClientAuth: tls.RequestClientCert, 35 | MinVersion: tls.VersionTLS13, 36 | InsecureSkipVerify: true, 37 | 38 | GetClientCertificate: func(req *tls.CertificateRequestInfo) (*tls.Certificate, error) { 39 | if myKey == nil { 40 | return &tls.Certificate{}, nil 41 | } 42 | 43 | certDER, err := newSelfSignedCert(myKey) 44 | if err != nil { 45 | return nil, errors.New("error generating self-signed certificate: %s", err) 46 | } 47 | cert := &tls.Certificate{ 48 | Certificate: [][]byte{certDER}, 49 | PrivateKey: myKey, 50 | } 51 | return cert, nil 52 | }, 53 | 54 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 55 | if len(rawCerts) == 0 { 56 | return ErrNoPeerCertificates 57 | } 58 | 59 | if len(rawCerts) != 1 { 60 | return errors.New("too many peer certificates: %d", len(rawCerts)) 61 | } 62 | 63 | cert, err := x509.ParseCertificate(rawCerts[0]) 64 | if err != nil { 65 | return errors.Wrap(err, "x509.ParseCertificate") 66 | } 67 | 68 | if err := cert.CheckSignatureFrom(cert); err != nil { 69 | return ErrVerificationFailed 70 | } 71 | theirKey, ok := cert.PublicKey.(ed25519.PublicKey) 72 | if !ok { 73 | return errors.New("invalid public key type in certificate: %T", cert.PublicKey) 74 | } 75 | if !bytes.Equal(theirKey, peerKey) { 76 | return ErrVerificationFailed 77 | } 78 | 79 | return nil 80 | }, 81 | } 82 | 83 | return config 84 | } 85 | -------------------------------------------------------------------------------- /typesocket/client.go: -------------------------------------------------------------------------------- 1 | package typesocket 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/json" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | 12 | "vuvuzela.io/alpenhorn/edtls" 13 | "vuvuzela.io/alpenhorn/log" 14 | ) 15 | 16 | type ClientConn struct { 17 | mu sync.Mutex 18 | ws *websocket.Conn 19 | } 20 | 21 | type Conn interface { 22 | Send(msgID string, v interface{}) error 23 | 24 | Close() error 25 | } 26 | 27 | func Dial(addr string, peerKey ed25519.PublicKey) (*ClientConn, error) { 28 | tlsConfig := edtls.NewTLSClientConfig(nil, peerKey) 29 | 30 | dialer := &websocket.Dialer{ 31 | TLSClientConfig: tlsConfig, 32 | HandshakeTimeout: 10 * time.Second, 33 | } 34 | ws, _, err := dialer.Dial(addr, nil) 35 | if err != nil { 36 | return nil, err 37 | } 38 | conn := &ClientConn{ 39 | ws: ws, 40 | } 41 | 42 | ws.SetReadDeadline(time.Now().Add(pongWait)) 43 | ws.SetPingHandler(conn.pingHandler) 44 | 45 | return conn, nil 46 | } 47 | 48 | func (c *ClientConn) pingHandler(message string) error { 49 | c.ws.SetReadDeadline(time.Now().Add(pongWait)) 50 | // The code below is copied from the default ping handler. 51 | err := c.ws.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(writeWait)) 52 | if err == websocket.ErrCloseSent { 53 | return nil 54 | } else if e, ok := err.(net.Error); ok && e.Temporary() { 55 | return nil 56 | } 57 | return err 58 | } 59 | 60 | func (c *ClientConn) Close() error { 61 | c.mu.Lock() 62 | c.ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, "")) 63 | c.mu.Unlock() 64 | 65 | return c.ws.Close() 66 | } 67 | 68 | func (c *ClientConn) Send(msgID string, v interface{}) error { 69 | msg, err := json.Marshal(v) 70 | if err != nil { 71 | return err 72 | } 73 | e := &envelope{ 74 | ID: msgID, 75 | Message: msg, 76 | } 77 | 78 | c.mu.Lock() 79 | defer c.mu.Unlock() 80 | 81 | c.ws.SetWriteDeadline(time.Now().Add(writeWait)) 82 | if err := c.ws.WriteJSON(e); err != nil { 83 | log.WithFields(log.Fields{"call": "WriteJSON"}).Error(err) 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (c *ClientConn) Serve(mux Mux) error { 91 | defer c.Close() 92 | 93 | for { 94 | var e envelope 95 | if err := c.ws.ReadJSON(&e); err != nil { 96 | if websocket.IsCloseError(err, websocket.CloseGoingAway) { 97 | return err 98 | } 99 | return err 100 | } 101 | go mux.openEnvelope(c, &e) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/server_easyjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. 2 | 3 | package pkg 4 | 5 | import ( 6 | json "encoding/json" 7 | easyjson "github.com/davidlazar/easyjson" 8 | jlexer "github.com/davidlazar/easyjson/jlexer" 9 | jwriter "github.com/davidlazar/easyjson/jwriter" 10 | ) 11 | 12 | // suppress unused package warning 13 | var ( 14 | _ *json.RawMessage 15 | _ *jlexer.Lexer 16 | _ *jwriter.Writer 17 | _ easyjson.Marshaler 18 | ) 19 | 20 | func easyjsonDecodePublicServerConfig22b57fa5(in *jlexer.Lexer, out *PublicServerConfig) { 21 | isTopLevel := in.IsStart() 22 | if in.IsNull() { 23 | if isTopLevel { 24 | in.Consumed() 25 | } 26 | in.Skip() 27 | return 28 | } 29 | in.Delim('{') 30 | for !in.IsDelim('}') { 31 | key := in.UnsafeString() 32 | in.WantColon() 33 | if in.IsNull() { 34 | in.Skip() 35 | in.WantComma() 36 | continue 37 | } 38 | switch key { 39 | case "Key": 40 | if in.IsNull() { 41 | in.Skip() 42 | out.Key = nil 43 | } else { 44 | out.Key = in.BytesReadable() 45 | } 46 | case "Address": 47 | out.Address = string(in.String()) 48 | default: 49 | in.SkipRecursive() 50 | } 51 | in.WantComma() 52 | } 53 | in.Delim('}') 54 | if isTopLevel { 55 | in.Consumed() 56 | } 57 | } 58 | func easyjsonEncodePublicServerConfig22b57fa5(out *jwriter.Writer, in PublicServerConfig) { 59 | out.RawByte('{') 60 | first := true 61 | _ = first 62 | if !first { 63 | out.RawByte(',') 64 | } 65 | first = false 66 | out.RawString("\"Key\":") 67 | out.Base32Bytes(in.Key) 68 | if !first { 69 | out.RawByte(',') 70 | } 71 | first = false 72 | out.RawString("\"Address\":") 73 | out.String(string(in.Address)) 74 | out.RawByte('}') 75 | } 76 | 77 | // MarshalJSON supports json.Marshaler interface 78 | func (v PublicServerConfig) MarshalJSON() ([]byte, error) { 79 | w := jwriter.Writer{} 80 | easyjsonEncodePublicServerConfig22b57fa5(&w, v) 81 | return w.Buffer.BuildBytes(), w.Error 82 | } 83 | 84 | // MarshalEasyJSON supports easyjson.Marshaler interface 85 | func (v PublicServerConfig) MarshalEasyJSON(w *jwriter.Writer) { 86 | easyjsonEncodePublicServerConfig22b57fa5(w, v) 87 | } 88 | 89 | // UnmarshalJSON supports json.Unmarshaler interface 90 | func (v *PublicServerConfig) UnmarshalJSON(data []byte) error { 91 | r := jlexer.Lexer{Data: data} 92 | easyjsonDecodePublicServerConfig22b57fa5(&r, v) 93 | return r.Error() 94 | } 95 | 96 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 97 | func (v *PublicServerConfig) UnmarshalEasyJSON(l *jlexer.Lexer) { 98 | easyjsonDecodePublicServerConfig22b57fa5(l, v) 99 | } 100 | -------------------------------------------------------------------------------- /edtls/server.go: -------------------------------------------------------------------------------- 1 | package edtls 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "fmt" 9 | "math/big" 10 | "net" 11 | "sync" 12 | "time" 13 | 14 | "vuvuzela.io/alpenhorn/errors" 15 | ) 16 | 17 | func Listen(network, laddr string, key ed25519.PrivateKey) (net.Listener, error) { 18 | config := NewTLSServerConfig(key) 19 | 20 | return tls.Listen(network, laddr, config) 21 | } 22 | 23 | func Server(conn net.Conn, key ed25519.PrivateKey) *tls.Conn { 24 | config := NewTLSServerConfig(key) 25 | 26 | return tls.Server(conn, config) 27 | } 28 | 29 | func NewTLSServerConfig(key ed25519.PrivateKey) *tls.Config { 30 | var mu sync.Mutex 31 | var expiry time.Time 32 | var currCert *tls.Certificate 33 | 34 | var config = &tls.Config{ 35 | GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 36 | mu.Lock() 37 | defer mu.Unlock() 38 | 39 | if currCert != nil && time.Now().Before(expiry) { 40 | return currCert, nil 41 | } 42 | 43 | certDER, err := newSelfSignedCert(key) 44 | if err != nil { 45 | return nil, fmt.Errorf("error generating self-signed certificate: %s", err) 46 | } 47 | 48 | currCert = &tls.Certificate{ 49 | Certificate: [][]byte{certDER}, 50 | PrivateKey: key, 51 | } 52 | expiry = time.Now().Add(2 * certDuration / 3) 53 | return currCert, nil 54 | }, 55 | 56 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { 57 | if len(rawCerts) == 0 { 58 | return nil 59 | } 60 | 61 | if len(rawCerts) != 1 { 62 | return errors.New("too many peer certificates: %d", len(rawCerts)) 63 | } 64 | 65 | cert, err := x509.ParseCertificate(rawCerts[0]) 66 | if err != nil { 67 | return errors.Wrap(err, "x509.ParseCertificate") 68 | } 69 | 70 | if err := cert.CheckSignatureFrom(cert); err != nil { 71 | return ErrVerificationFailed 72 | } 73 | 74 | return nil 75 | }, 76 | 77 | RootCAs: x509.NewCertPool(), 78 | ClientAuth: tls.RequestClientCert, 79 | MinVersion: tls.VersionTLS13, 80 | } 81 | 82 | return config 83 | } 84 | 85 | var certDuration = 1 * time.Hour 86 | 87 | func newSelfSignedCert(key ed25519.PrivateKey) ([]byte, error) { 88 | // generate a self-signed cert 89 | now := time.Now() 90 | expiry := now.Add(certDuration) 91 | template := &x509.Certificate{ 92 | SerialNumber: new(big.Int), 93 | NotBefore: now.UTC().AddDate(0, 0, -1), 94 | NotAfter: expiry.UTC(), 95 | 96 | BasicConstraintsValid: true, 97 | IsCA: true, 98 | 99 | KeyUsage: x509.KeyUsageCertSign, 100 | } 101 | 102 | certDER, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return certDER, nil 108 | } 109 | -------------------------------------------------------------------------------- /keywheel/keywheel_easyjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. 2 | 3 | package keywheel 4 | 5 | import ( 6 | json "encoding/json" 7 | easyjson "github.com/davidlazar/easyjson" 8 | jlexer "github.com/davidlazar/easyjson/jlexer" 9 | jwriter "github.com/davidlazar/easyjson/jwriter" 10 | ) 11 | 12 | // suppress unused package warning 13 | var ( 14 | _ *json.RawMessage 15 | _ *jlexer.Lexer 16 | _ *jwriter.Writer 17 | _ easyjson.Marshaler 18 | ) 19 | 20 | func easyjsonDecodeRoundSecret2aeb2176(in *jlexer.Lexer, out *roundSecret) { 21 | isTopLevel := in.IsStart() 22 | if in.IsNull() { 23 | if isTopLevel { 24 | in.Consumed() 25 | } 26 | in.Skip() 27 | return 28 | } 29 | in.Delim('{') 30 | for !in.IsDelim('}') { 31 | key := in.UnsafeString() 32 | in.WantColon() 33 | if in.IsNull() { 34 | in.Skip() 35 | in.WantComma() 36 | continue 37 | } 38 | switch key { 39 | case "Round": 40 | out.Round = uint32(in.Uint32()) 41 | case "Secret": 42 | if in.IsNull() { 43 | in.Skip() 44 | out.Secret = nil 45 | } else { 46 | if out.Secret == nil { 47 | out.Secret = new([32]uint8) 48 | } 49 | if in.IsNull() { 50 | in.Skip() 51 | } else { 52 | copy((*out.Secret)[:], in.BytesReadable()) 53 | } 54 | } 55 | default: 56 | in.SkipRecursive() 57 | } 58 | in.WantComma() 59 | } 60 | in.Delim('}') 61 | if isTopLevel { 62 | in.Consumed() 63 | } 64 | } 65 | func easyjsonEncodeRoundSecret2aeb2176(out *jwriter.Writer, in roundSecret) { 66 | out.RawByte('{') 67 | first := true 68 | _ = first 69 | if !first { 70 | out.RawByte(',') 71 | } 72 | first = false 73 | out.RawString("\"Round\":") 74 | out.Uint32(uint32(in.Round)) 75 | if !first { 76 | out.RawByte(',') 77 | } 78 | first = false 79 | out.RawString("\"Secret\":") 80 | if in.Secret == nil { 81 | out.RawString("null") 82 | } else { 83 | out.Base32Bytes((*in.Secret)[:]) 84 | } 85 | out.RawByte('}') 86 | } 87 | 88 | // MarshalJSON supports json.Marshaler interface 89 | func (v roundSecret) MarshalJSON() ([]byte, error) { 90 | w := jwriter.Writer{} 91 | easyjsonEncodeRoundSecret2aeb2176(&w, v) 92 | return w.Buffer.BuildBytes(), w.Error 93 | } 94 | 95 | // MarshalEasyJSON supports easyjson.Marshaler interface 96 | func (v roundSecret) MarshalEasyJSON(w *jwriter.Writer) { 97 | easyjsonEncodeRoundSecret2aeb2176(w, v) 98 | } 99 | 100 | // UnmarshalJSON supports json.Unmarshaler interface 101 | func (v *roundSecret) UnmarshalJSON(data []byte) error { 102 | r := jlexer.Lexer{Data: data} 103 | easyjsonDecodeRoundSecret2aeb2176(&r, v) 104 | return r.Error() 105 | } 106 | 107 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 108 | func (v *roundSecret) UnmarshalEasyJSON(l *jlexer.Lexer) { 109 | easyjsonDecodeRoundSecret2aeb2176(l, v) 110 | } 111 | -------------------------------------------------------------------------------- /log/ansi/ansi.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ansi implements ANSI escape codes for terminal colors. 6 | package ansi 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "strings" 13 | ) 14 | 15 | // Code is an ANSI escape code. 16 | type Code string 17 | 18 | const ( 19 | Reset Code = "0" 20 | Bold Code = "1" 21 | Reverse Code = "7" 22 | Red Code = "38;5;1" 23 | Green Code = "38;5;2" 24 | Yellow Code = "38;5;3" 25 | Blue Code = "38;5;4" 26 | Magenta Code = "38;5;5" 27 | Cyan Code = "38;5;6" 28 | White Code = "38;5;7" // Usually light grey. 29 | ) 30 | 31 | var AllColors = []Code{Red, Green, Yellow, Blue, Magenta, Cyan, White} 32 | 33 | // Foreground returns a color from [0,255]. 34 | func Foreground(color int) Code { 35 | return Code(fmt.Sprintf("38;5;%d", color)) 36 | } 37 | 38 | type ansiFormatter struct { 39 | value interface{} 40 | codes []Code 41 | } 42 | 43 | // Colorf returns an fmt.Formatter that colors the value according 44 | // to the codes when passed to an fmt "printf" function. For example: 45 | // fmt.Printf("%d %s", Colorf(42, Blue), Colorf(err, Red)). If codes 46 | // is empty, Colorf returns the original value for efficiency. 47 | func Colorf(value interface{}, codes ...Code) interface{} { 48 | if len(codes) == 0 { 49 | return value 50 | } 51 | return &ansiFormatter{value, codes} 52 | } 53 | 54 | func (af *ansiFormatter) Format(f fmt.State, c rune) { 55 | // reconstruct the format string in bf 56 | bf := new(bytes.Buffer) 57 | bf.WriteByte('%') 58 | for _, x := range []byte{'-', '+', '#', ' ', '0'} { 59 | if f.Flag(int(x)) { 60 | bf.WriteByte(x) 61 | } 62 | } 63 | if w, ok := f.Width(); ok { 64 | fmt.Fprint(bf, w) 65 | } 66 | if p, ok := f.Precision(); ok { 67 | fmt.Fprintf(bf, ".%d", p) 68 | } 69 | bf.WriteRune(c) 70 | format := bf.String() 71 | 72 | if len(af.codes) == 0 { 73 | fmt.Fprintf(f, format, af.value) 74 | return 75 | } 76 | 77 | fmt.Fprintf(f, "\x1b[%sm", joinCodes(af.codes)) 78 | fmt.Fprintf(f, format, af.value) 79 | fmt.Fprint(f, "\x1b[0m") 80 | } 81 | 82 | func WriteString(dst io.Writer, str string, codes ...Code) (n int, err error) { 83 | if len(codes) == 0 { 84 | return io.WriteString(dst, str) 85 | } 86 | 87 | n, err = fmt.Fprintf(dst, "\x1b[%sm", joinCodes(codes)) 88 | if err != nil { 89 | return 90 | } 91 | 92 | nn, err := io.WriteString(dst, str) 93 | n += nn 94 | if err != nil { 95 | return 96 | } 97 | 98 | nn, err = fmt.Fprint(dst, "\x1b[0m") 99 | n += nn 100 | return 101 | } 102 | 103 | func joinCodes(codes []Code) string { 104 | strs := make([]string, len(codes)) 105 | for i := range strs { 106 | strs[i] = string(codes[i]) 107 | } 108 | return strings.Join(strs, ";") 109 | } 110 | -------------------------------------------------------------------------------- /pkg/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | ) 12 | 13 | //go:generate stringer -type=ErrorCode 14 | type ErrorCode int 15 | 16 | const ( 17 | ErrBadRequestJSON ErrorCode = iota + 1 18 | ErrDatabaseError 19 | ErrInvalidUsername 20 | ErrInvalidLoginKey 21 | ErrNotRegistered 22 | ErrAlreadyRegistered 23 | ErrRoundNotFound 24 | ErrInvalidUserLongTermKey 25 | ErrInvalidSignature 26 | ErrInvalidToken 27 | ErrExpiredToken 28 | ErrUnauthorized 29 | ErrBadCommitment 30 | 31 | ErrUnknown 32 | ) 33 | 34 | var errText = map[ErrorCode]string{ 35 | ErrBadRequestJSON: "invalid json in request", 36 | ErrDatabaseError: "internal database error", 37 | ErrInvalidUsername: "invalid username", 38 | ErrInvalidLoginKey: "invalid login key", 39 | ErrNotRegistered: "username not registered", 40 | ErrAlreadyRegistered: "username already registered", 41 | ErrRoundNotFound: "round not found", 42 | ErrInvalidUserLongTermKey: "invalid user long term key", 43 | ErrInvalidSignature: "invalid signature", 44 | ErrInvalidToken: "invalid token", 45 | ErrExpiredToken: "expired token", 46 | ErrUnauthorized: "unauthorized", 47 | ErrBadCommitment: "bad commitment", 48 | 49 | ErrUnknown: "unknown error", 50 | } 51 | 52 | func (e ErrorCode) httpCode() int { 53 | switch e { 54 | case ErrDatabaseError, ErrUnknown: 55 | return http.StatusInternalServerError 56 | case ErrUnauthorized: 57 | return http.StatusUnauthorized 58 | default: 59 | return http.StatusBadRequest 60 | } 61 | } 62 | 63 | func errorCode(err error) ErrorCode { 64 | switch err := err.(type) { 65 | case Error: 66 | return err.Code 67 | default: 68 | return ErrUnknown 69 | } 70 | } 71 | 72 | func isInternalError(err error) bool { 73 | switch errorCode(err) { 74 | case ErrDatabaseError, ErrBadCommitment, ErrUnknown: 75 | return true 76 | } 77 | return false 78 | } 79 | 80 | type Error struct { 81 | Code ErrorCode 82 | Message string 83 | } 84 | 85 | func (e Error) Error() string { 86 | txt, ok := errText[e.Code] 87 | if !ok { 88 | return "unknown error code" 89 | } 90 | if e.Message == "" { 91 | return txt 92 | } 93 | return fmt.Sprintf("%s: %s", txt, e.Message) 94 | } 95 | 96 | func errorf(code ErrorCode, format string, args ...interface{}) Error { 97 | return Error{ 98 | Code: code, 99 | Message: fmt.Sprintf(format, args...), 100 | } 101 | } 102 | 103 | func httpError(w http.ResponseWriter, err error) { 104 | var pkgError Error 105 | switch v := err.(type) { 106 | case Error: 107 | pkgError = v 108 | default: 109 | pkgError = Error{errorCode(err), err.Error()} 110 | } 111 | 112 | data, err := json.Marshal(pkgError) 113 | if err != nil { 114 | // this shouldn't happen 115 | panic(err) 116 | } 117 | httpCode := pkgError.Code.httpCode() 118 | w.WriteHeader(httpCode) 119 | w.Write(data) 120 | } 121 | -------------------------------------------------------------------------------- /cmd/guardian/alpenhorn-guardian-keygen/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "crypto/rand" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | 17 | "github.com/davidlazar/go-crypto/encoding/base32" 18 | "golang.org/x/crypto/nacl/secretbox" 19 | "golang.org/x/crypto/ssh/terminal" 20 | 21 | "vuvuzela.io/alpenhorn/cmd/guardian" 22 | ) 23 | 24 | var inspirationalMessage = ` 25 | !! You are generating an Alpenhorn guardian key. 26 | !! This key is crucial to the security of Alpenhorn. 27 | !! Millions of users are counting on you. Pick a STRONG passphrase. 28 | 29 | ` 30 | 31 | func main() { 32 | appDir := guardian.Appdir() 33 | err := os.Mkdir(appDir, 0700) 34 | if err == nil { 35 | fmt.Printf("Created directory %s\n", appDir) 36 | } else if !os.IsExist(err) { 37 | log.Fatal(err) 38 | } 39 | 40 | privatePath := filepath.Join(appDir, "guardian.privatekey") 41 | publicPath := filepath.Join(appDir, "guardian.publickey") 42 | checkOverwrite(privatePath) 43 | checkOverwrite(publicPath) 44 | 45 | fmt.Fprintf(os.Stdout, inspirationalMessage) 46 | pw := confirmPassphrase() 47 | fmt.Println() 48 | 49 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | dk := guardian.DeriveKey(pw) 55 | var boxKey [32]byte 56 | copy(boxKey[:], dk) 57 | var nonce [24]byte 58 | _, err = rand.Read(nonce[:]) 59 | if err != nil { 60 | panic(err) 61 | } 62 | msg := privateKey[:] 63 | ctxt := secretbox.Seal(nonce[:], msg, &nonce, &boxKey) 64 | 65 | err = ioutil.WriteFile(publicPath, []byte(base32.EncodeToString(publicKey[:])+"\n"), 0600) 66 | if err != nil { 67 | log.Fatalf("failed to write public key: %s", err) 68 | } 69 | fmt.Printf("Wrote public key: %s\n", publicPath) 70 | 71 | err = ioutil.WriteFile(privatePath, []byte(base32.EncodeToString(ctxt)+"\n"), 0600) 72 | if err != nil { 73 | log.Fatalf("failed to write private key: %s", err) 74 | } 75 | fmt.Printf("Wrote private key: %s\n", privatePath) 76 | 77 | fmt.Printf("\n!! You should make a backup of the private key before sharing the public key.\n") 78 | } 79 | 80 | func confirmPassphrase() []byte { 81 | for { 82 | fmt.Fprintf(os.Stderr, "Enter passphrase: ") 83 | pw, err := terminal.ReadPassword(0) 84 | fmt.Fprintln(os.Stderr) 85 | if err != nil { 86 | log.Fatalf("terminal.ReadPassword: %s", err) 87 | } 88 | 89 | if len(pw) == 0 { 90 | continue 91 | } 92 | 93 | fmt.Fprintf(os.Stderr, "Enter same passphrase again: ") 94 | again, err := terminal.ReadPassword(0) 95 | fmt.Fprintln(os.Stderr) 96 | if err != nil { 97 | log.Fatalf("terminal.ReadPassword: %s", err) 98 | } 99 | 100 | if bytes.Equal(pw, again) { 101 | return pw 102 | } 103 | 104 | fmt.Fprintf(os.Stderr, "Passphrases do not match. Try again.\n") 105 | } 106 | } 107 | 108 | func checkOverwrite(path string) { 109 | _, err := os.Stat(path) 110 | if os.IsNotExist(err) { 111 | return 112 | } 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | fmt.Printf("%s already exists. Refusing to overwrite.\n", path) 117 | os.Exit(1) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/alpenhorn-config-server/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "crypto/tls" 9 | "encoding/json" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | "time" 18 | 19 | "golang.org/x/crypto/acme/autocert" 20 | 21 | "vuvuzela.io/alpenhorn/config" 22 | // Register the convo inner config. 23 | _ "vuvuzela.io/vuvuzela/convo" 24 | ) 25 | 26 | var ( 27 | hostname = flag.String("hostname", "", "hostname of config server") 28 | setConfigPath = flag.String("setConfig", "", "path to signed config to make current") 29 | persistPath = flag.String("persist", "persist_config_server", "persistent data directory") 30 | ) 31 | 32 | func main() { 33 | flag.Parse() 34 | 35 | if err := os.MkdirAll(*persistPath, 0700); err != nil { 36 | log.Fatal(err) 37 | } 38 | serverPath := filepath.Join(*persistPath, "config-server-state") 39 | 40 | if *setConfigPath != "" { 41 | setConfig(serverPath) 42 | return 43 | } 44 | 45 | server, err := config.LoadServer(serverPath) 46 | if os.IsNotExist(err) { 47 | fmt.Println("No server state found. Please initialize server with -setConfig.") 48 | os.Exit(1) 49 | } else if err != nil { 50 | log.Fatalf("error loading server state: %s", err) 51 | } 52 | 53 | if *hostname == "" { 54 | fmt.Println("Please set -hostname.") 55 | os.Exit(1) 56 | } 57 | 58 | certManager := autocert.Manager{ 59 | Cache: autocert.DirCache(filepath.Join(*persistPath, "ssl")), 60 | Prompt: autocert.AcceptTOS, 61 | HostPolicy: autocert.HostWhitelist(*hostname), 62 | } 63 | // Listen on :80 for http-01 ACME challenge. 64 | go http.ListenAndServe(":http", certManager.HTTPHandler(nil)) 65 | 66 | httpServer := &http.Server{ 67 | Addr: ":https", 68 | Handler: server, 69 | TLSConfig: &tls.Config{GetCertificate: certManager.GetCertificate}, 70 | 71 | ReadTimeout: 10 * time.Second, 72 | WriteTimeout: 10 * time.Second, 73 | } 74 | log.Printf("Listening on https://%s", *hostname) 75 | log.Fatal(httpServer.ListenAndServeTLS("", "")) 76 | } 77 | 78 | func setConfig(serverPath string) { 79 | data, err := ioutil.ReadFile(*setConfigPath) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | conf := new(config.SignedConfig) 85 | err = json.Unmarshal(data, conf) 86 | if err != nil { 87 | log.Fatalf("error decoding config: %s", err) 88 | } 89 | 90 | server, err := config.LoadServer(serverPath) 91 | if err == nil { 92 | err = server.SetCurrentConfig(conf) 93 | if err != nil { 94 | log.Fatalf("error setting config: %s", err) 95 | } 96 | fmt.Printf("Set current %q config in existing server state.\n", conf.Service) 97 | } else if os.IsNotExist(err) { 98 | server, err := config.CreateServer(serverPath) 99 | if err != nil { 100 | log.Fatalf("error creating server state: %s", err) 101 | } 102 | fmt.Printf("Created new config server state: %s\n", serverPath) 103 | 104 | err = server.SetCurrentConfig(conf) 105 | if err != nil { 106 | log.Fatalf("error setting config: %s", err) 107 | } 108 | fmt.Printf("Set current %q config in new state.\n", conf.Service) 109 | } else { 110 | log.Fatalf("unexpected error loading server state: %s", err) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /edhttp/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package edhttp is an HTTP client that connects to HTTP servers 6 | // on edtls listeners. 7 | package edhttp 8 | 9 | import ( 10 | "bytes" 11 | "context" 12 | "crypto/ed25519" 13 | "encoding/json" 14 | "io" 15 | "net" 16 | "net/http" 17 | "net/url" 18 | "sync" 19 | 20 | "vuvuzela.io/alpenhorn/edtls" 21 | "vuvuzela.io/alpenhorn/errors" 22 | ) 23 | 24 | type Client struct { 25 | Key ed25519.PrivateKey 26 | 27 | initOnce sync.Once 28 | client *http.Client 29 | 30 | mu sync.RWMutex 31 | serverKeys map[string]ed25519.PublicKey 32 | } 33 | 34 | func (c *Client) init() { 35 | c.initOnce.Do(func() { 36 | c.serverKeys = make(map[string]ed25519.PublicKey) 37 | 38 | c.client = &http.Client{ 39 | Transport: &http.Transport{ 40 | DialTLS: func(network, addr string) (net.Conn, error) { 41 | c.mu.RLock() 42 | serverKey := c.serverKeys[addr] 43 | c.mu.RUnlock() 44 | if serverKey == nil { 45 | return nil, errors.New("no edtls key for %s", addr) 46 | } 47 | return edtls.Dial(network, addr, serverKey, c.Key) 48 | }, 49 | 50 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 51 | return nil, errors.New("edhttp does not allow unencrypted tcp connections") 52 | }, 53 | }, 54 | } 55 | }) 56 | } 57 | 58 | // assertKey tells the client to expect an edTLS certificate 59 | // signed by key when connecting to the given address. 60 | func (c *Client) assertKey(address string, key ed25519.PublicKey) error { 61 | c.mu.Lock() 62 | defer c.mu.Unlock() 63 | 64 | k := c.serverKeys[address] 65 | if k != nil { 66 | if bytes.Equal(k, key) { 67 | return nil 68 | } 69 | return errors.New("multiple keys for address: %s", address) 70 | } 71 | 72 | c.serverKeys[address] = key 73 | return nil 74 | } 75 | 76 | func (c *Client) assertKeyURL(urlStr string, key ed25519.PublicKey) error { 77 | u, err := url.Parse(urlStr) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return c.assertKey(u.Host, key) 83 | } 84 | 85 | func (c *Client) Do(key ed25519.PublicKey, req *http.Request) (*http.Response, error) { 86 | c.init() 87 | if err := c.assertKey(req.URL.Host, key); err != nil { 88 | return nil, err 89 | } 90 | return c.client.Do(req) 91 | } 92 | 93 | func (c *Client) Get(key ed25519.PublicKey, url string) (*http.Response, error) { 94 | c.init() 95 | if err := c.assertKeyURL(url, key); err != nil { 96 | return nil, err 97 | } 98 | return c.client.Get(url) 99 | } 100 | 101 | func (c *Client) Post(key ed25519.PublicKey, url string, contentType string, body io.Reader) (*http.Response, error) { 102 | c.init() 103 | if err := c.assertKeyURL(url, key); err != nil { 104 | return nil, err 105 | } 106 | return c.client.Post(url, contentType, body) 107 | } 108 | 109 | func (c *Client) PostJSON(key ed25519.PublicKey, url string, v interface{}) (*http.Response, error) { 110 | c.init() 111 | if err := c.assertKeyURL(url, key); err != nil { 112 | return nil, err 113 | } 114 | 115 | buf := new(bytes.Buffer) 116 | err := json.NewEncoder(buf).Encode(v) 117 | if err != nil { 118 | return nil, errors.New("json encoding error: %s", err) 119 | } 120 | 121 | return c.client.Post(url, "application/json", buf) 122 | } 123 | -------------------------------------------------------------------------------- /bloom/bloom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package bloom implements Bloom filters. 6 | package bloom 7 | 8 | import ( 9 | "encoding/binary" 10 | "encoding/json" 11 | "errors" 12 | "math" 13 | 14 | "github.com/dchest/siphash" 15 | ) 16 | 17 | type Filter struct { 18 | numHashes int 19 | data []byte 20 | } 21 | 22 | func New(sizeBits int, numHashes int) *Filter { 23 | m := (sizeBits + 7) / 8 24 | return &Filter{ 25 | numHashes: numHashes, 26 | data: make([]byte, m), 27 | } 28 | } 29 | 30 | // Optimal computes optimal Bloom filter parameters. 31 | // These parameters are optimal for small bloom filters as 32 | // described in section 4.1 of this paper: 33 | // 34 | // https://web.stanford.edu/~ashishg/papers/inverted.pdf 35 | func Optimal(numElements int, falsePositiveRate float64) (sizeBits int, numHashes int) { 36 | n := float64(numElements) 37 | p := falsePositiveRate 38 | m := -(n+0.5)*math.Log(p)/math.Pow(math.Log(2), 2) + 1 39 | k := -math.Log(p) / math.Log(2) 40 | return int(math.Ceil(m)), int(math.Ceil(k)) 41 | } 42 | 43 | func (f *Filter) Set(x []byte) { 44 | hs := hash(x, f.numHashes) 45 | n := uint32(len(f.data) * 8) 46 | for _, h := range hs { 47 | f.set(h % n) 48 | } 49 | } 50 | 51 | func (f *Filter) Test(x []byte) bool { 52 | hs := hash(x, f.numHashes) 53 | n := uint32(len(f.data) * 8) 54 | for _, h := range hs { 55 | if !f.test(h % n) { 56 | return false 57 | } 58 | } 59 | return true 60 | } 61 | 62 | func (f *Filter) Len() int { 63 | return len(f.data) 64 | } 65 | 66 | func (f *Filter) NumHashes() int { 67 | return f.numHashes 68 | } 69 | 70 | func (f *Filter) MarshalBinary() ([]byte, error) { 71 | data := make([]byte, len(f.data)+4) 72 | n := uint32(f.numHashes) 73 | binary.BigEndian.PutUint32(data[0:4], n) 74 | copy(data[4:], f.data) 75 | return data, nil 76 | } 77 | 78 | func (f *Filter) UnmarshalBinary(data []byte) error { 79 | if len(data) < 4 { 80 | return errors.New("short data") 81 | } 82 | f.numHashes = int(binary.BigEndian.Uint32(data[0:4])) 83 | f.data = data[4:] 84 | return nil 85 | } 86 | 87 | func (f *Filter) MarshalJSON() ([]byte, error) { 88 | data, err := f.MarshalBinary() 89 | if err != nil { 90 | return nil, err 91 | } 92 | return json.Marshal(data) 93 | } 94 | 95 | func (f *Filter) UnmarshalJSON(data []byte) error { 96 | var bs []byte 97 | if err := json.Unmarshal(data, &bs); err != nil { 98 | return err 99 | } 100 | return f.UnmarshalBinary(bs) 101 | } 102 | 103 | // Previously, we used the hashing method described in this paper: 104 | // http://www.eecs.harvard.edu/~michaelm/postscripts/rsa2008.pdf 105 | // but this gave us bad false positive rates for small bloom filters. 106 | func hash(x []byte, nhash int) []uint32 { 107 | res := make([]uint32, nhash+3) 108 | 109 | for i := 0; i < (nhash+3)/4; i++ { 110 | h1, h2 := siphash.Hash128(uint64(i), 666666, x) 111 | 112 | res[i*4] = uint32(h1) 113 | res[i*4+1] = uint32(h1 >> 32) 114 | res[i*4+2] = uint32(h2) 115 | res[i*4+3] = uint32(h2 >> 32) 116 | } 117 | 118 | return res[:nhash] 119 | } 120 | 121 | func (f *Filter) test(bit uint32) bool { 122 | i := bit / 8 123 | return f.data[i]&(1<<(bit%8)) != 0 124 | } 125 | 126 | func (f *Filter) set(bit uint32) { 127 | i := bit / 8 128 | f.data[i] |= 1 << (bit % 8) 129 | } 130 | -------------------------------------------------------------------------------- /internal/alplog/format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alplog 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "os" 12 | "sync" 13 | 14 | "vuvuzela.io/alpenhorn/log" 15 | "vuvuzela.io/alpenhorn/log/ansi" 16 | ) 17 | 18 | var bufPool = sync.Pool{ 19 | New: func() interface{} { 20 | return new(bytes.Buffer) 21 | }, 22 | } 23 | 24 | type ProductionOutput struct { 25 | dirHandler *log.OutputDir 26 | stderrHandler outputText 27 | } 28 | 29 | func NewProductionOutput(logsDir string) (ProductionOutput, error) { 30 | h := ProductionOutput{ 31 | stderrHandler: outputText{ 32 | dst: log.Stderr, 33 | }, 34 | } 35 | 36 | if logsDir != "" { 37 | err := os.MkdirAll(logsDir, 0770) 38 | if err != nil { 39 | return h, fmt.Errorf("failed to create logs directory: %s", err) 40 | } 41 | 42 | h.dirHandler = &log.OutputDir{ 43 | Dir: logsDir, 44 | } 45 | } 46 | 47 | return h, nil 48 | } 49 | 50 | func (h ProductionOutput) Name() string { 51 | if h.dirHandler == nil { 52 | return "[stderr]" 53 | } 54 | return h.dirHandler.Dir 55 | } 56 | 57 | func (h ProductionOutput) Fire(e *log.Entry) { 58 | if h.dirHandler != nil { 59 | h.dirHandler.Fire(e) 60 | 61 | // Only print errors to stderr. 62 | if e.Level > log.ErrorLevel { 63 | return 64 | } 65 | } 66 | h.stderrHandler.Fire(e) 67 | } 68 | 69 | type outputText struct { 70 | dst io.Writer 71 | } 72 | 73 | func OutputText(dst io.Writer) log.EntryHandler { 74 | return outputText{dst} 75 | } 76 | 77 | func (h outputText) Fire(e *log.Entry) { 78 | buf := bufPool.Get().(*bytes.Buffer) 79 | buf.Reset() 80 | 81 | prettyPrint(buf, e) 82 | 83 | _, err := h.dst.Write(buf.Bytes()) 84 | if err != nil { 85 | fmt.Fprintf(log.Stderr, "Error writing log entry: %s", err) 86 | } 87 | 88 | bufPool.Put(buf) 89 | } 90 | 91 | func prettyPrint(buf *bytes.Buffer, e *log.Entry) { 92 | color := e.Level.Color() 93 | if e.Level == log.InfoLevel { 94 | // Colorful timestamps on info messages are too distracting. 95 | buf.WriteString(e.Time.Format("2006-01-02 15:04:05")) 96 | } else { 97 | ansi.WriteString(buf, e.Time.Format("2006-01-02 15:04:05"), color, ansi.Bold) 98 | } 99 | 100 | fmt.Fprintf(buf, " %s ", e.Level.Icon()) 101 | 102 | fields := make(log.Fields, len(e.Fields)) 103 | for k, v := range e.Fields { 104 | fields[k] = v 105 | } 106 | 107 | service, okService := fields["service"].(string) 108 | round, okRound := fields["round"].(uint32) 109 | if okService && okRound { 110 | delete(fields, "round") 111 | delete(fields, "service") 112 | l := len(ansi.AllColors) 113 | if service == "AddFriend" { 114 | roundColor := ansi.AllColors[int(round)%l] 115 | fmt.Fprintf(buf, "%05d ", ansi.Colorf(round, roundColor, ansi.Reverse)) 116 | } else { 117 | roundColor := ansi.AllColors[(int(round)+l/2)%l] 118 | fmt.Fprintf(buf, "%05d ", ansi.Colorf(round, roundColor)) 119 | } 120 | } 121 | 122 | tag, okTag := fields["tag"].(string) 123 | if okTag { 124 | delete(fields, "tag") 125 | tag = tag + ": " 126 | } 127 | 128 | rpc, okRPC := fields["rpc"].(string) 129 | if okRPC { 130 | delete(fields, "rpc") 131 | rpc = rpc + ": " 132 | } 133 | 134 | fmt.Fprintf(buf, "%-42s ", tag+rpc+e.Message) 135 | log.Logfmt(buf, fields, color) 136 | buf.WriteByte('\n') 137 | } 138 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package log provides structured logging. 6 | package log 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "time" 12 | ) 13 | 14 | type Logger struct { 15 | EntryHandler 16 | Level Level 17 | 18 | fields Fields 19 | } 20 | 21 | type Entry struct { 22 | Fields Fields 23 | Time time.Time 24 | Level Level 25 | Message string 26 | } 27 | 28 | type EntryHandler interface { 29 | Fire(*Entry) 30 | } 31 | 32 | func (l *Logger) Clone() *Logger { 33 | return &Logger{ 34 | EntryHandler: l.EntryHandler, 35 | Level: l.Level, 36 | fields: l.fields, 37 | } 38 | } 39 | 40 | type Fields map[string]interface{} 41 | 42 | func (l *Logger) WithFields(fields Fields) *Logger { 43 | ll := &Logger{ 44 | EntryHandler: l.EntryHandler, 45 | Level: l.Level, 46 | fields: make(Fields, len(l.fields)+len(fields)), 47 | } 48 | for k, v := range l.fields { 49 | ll.fields[k] = v 50 | } 51 | for k, v := range fields { 52 | ll.fields[k] = v 53 | } 54 | return ll 55 | } 56 | 57 | func (l *Logger) Info(args ...interface{}) { 58 | if l.Level >= InfoLevel { 59 | l.fire(InfoLevel, fmt.Sprint(args...)) 60 | } 61 | } 62 | 63 | func (l *Logger) Infof(format string, args ...interface{}) { 64 | if l.Level >= InfoLevel { 65 | l.fire(InfoLevel, fmt.Sprintf(format, args...)) 66 | } 67 | } 68 | 69 | func (l *Logger) Error(args ...interface{}) { 70 | if l.Level >= ErrorLevel { 71 | l.fire(ErrorLevel, fmt.Sprint(args...)) 72 | } 73 | } 74 | 75 | func (l *Logger) Errorf(format string, args ...interface{}) { 76 | if l.Level >= ErrorLevel { 77 | l.fire(ErrorLevel, fmt.Sprintf(format, args...)) 78 | } 79 | } 80 | 81 | func (l *Logger) Warn(args ...interface{}) { 82 | if l.Level >= WarnLevel { 83 | l.fire(WarnLevel, fmt.Sprint(args...)) 84 | } 85 | } 86 | 87 | func (l *Logger) Warnf(format string, args ...interface{}) { 88 | if l.Level >= WarnLevel { 89 | l.fire(WarnLevel, fmt.Sprintf(format, args...)) 90 | } 91 | } 92 | 93 | func (l *Logger) Fatal(args ...interface{}) { 94 | if l.Level >= FatalLevel { 95 | l.fire(FatalLevel, fmt.Sprint(args...)) 96 | } 97 | os.Exit(1) 98 | } 99 | 100 | func (l *Logger) Fatalf(format string, args ...interface{}) { 101 | if l.Level >= FatalLevel { 102 | l.fire(FatalLevel, fmt.Sprintf(format, args...)) 103 | } 104 | os.Exit(1) 105 | } 106 | 107 | func (l *Logger) Debug(args ...interface{}) { 108 | if l.Level >= DebugLevel { 109 | l.fire(DebugLevel, fmt.Sprint(args...)) 110 | } 111 | } 112 | 113 | func (l *Logger) Debugf(format string, args ...interface{}) { 114 | if l.Level >= DebugLevel { 115 | l.fire(DebugLevel, fmt.Sprintf(format, args...)) 116 | } 117 | } 118 | 119 | func (l *Logger) Panic(args ...interface{}) { 120 | msg := fmt.Sprint(args...) 121 | if l.Level >= PanicLevel { 122 | l.fire(PanicLevel, msg) 123 | } 124 | panic(msg) 125 | } 126 | 127 | func (l *Logger) Panicf(format string, args ...interface{}) { 128 | msg := fmt.Sprintf(format, args...) 129 | if l.Level >= PanicLevel { 130 | l.fire(PanicLevel, msg) 131 | } 132 | panic(msg) 133 | } 134 | 135 | func (l *Logger) fire(level Level, msg string) { 136 | if l.EntryHandler != nil { 137 | entry := &Entry{ 138 | Fields: l.fields, 139 | Time: time.Now(), 140 | Level: level, 141 | Message: msg, 142 | } 143 | l.Fire(entry) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /pkg/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "encoding/json" 11 | "net/http" 12 | 13 | "github.com/dgraph-io/badger" 14 | 15 | "vuvuzela.io/alpenhorn/bloom" 16 | "vuvuzela.io/alpenhorn/log" 17 | ) 18 | 19 | type statusArgs struct { 20 | Username string 21 | Message [32]byte 22 | ServerSigningKey ed25519.PublicKey `json:"-"` 23 | 24 | Signature []byte 25 | } 26 | 27 | func (a *statusArgs) msg() []byte { 28 | buf := new(bytes.Buffer) 29 | buf.WriteString("StatusArgs") 30 | buf.Write(a.ServerSigningKey) 31 | id := ValidUsernameToIdentity(a.Username) 32 | buf.Write(id[:]) 33 | buf.Write(a.Message[:]) 34 | return buf.Bytes() 35 | } 36 | 37 | type statusReply struct { 38 | } 39 | 40 | func (srv *Server) statusHandler(w http.ResponseWriter, req *http.Request) { 41 | body := http.MaxBytesReader(w, req.Body, 512) 42 | args := new(statusArgs) 43 | err := json.NewDecoder(body).Decode(args) 44 | if err != nil { 45 | httpError(w, errorf(ErrBadRequestJSON, "%s", err)) 46 | return 47 | } 48 | args.ServerSigningKey = srv.publicKey 49 | 50 | reply, err := srv.checkStatus(args) 51 | if err != nil { 52 | if isInternalError(err) { 53 | srv.log.WithFields(log.Fields{ 54 | "username": args.Username, 55 | "code": errorCode(err).String(), 56 | }).Errorf("Status failed: %s", err) 57 | } 58 | httpError(w, err) 59 | return 60 | } 61 | 62 | bs, err := json.Marshal(reply) 63 | if err != nil { 64 | panic(err) 65 | } 66 | w.Write(bs) 67 | } 68 | 69 | func (srv *Server) checkStatus(args *statusArgs) (*statusReply, error) { 70 | user, _, err := srv.getUser(nil, args.Username) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | if !ed25519.Verify(user.LoginKey, args.msg(), args.Signature) { 76 | return nil, errorf(ErrInvalidSignature, "") 77 | } 78 | 79 | return &statusReply{}, nil 80 | } 81 | 82 | func (srv *Server) RegisteredUsernames() ([]*[64]byte, error) { 83 | users := make([]*[64]byte, 0, 32) 84 | err := srv.db.View(func(tx *badger.Txn) error { 85 | opt := badger.DefaultIteratorOptions 86 | it := tx.NewIterator(opt) 87 | defer it.Close() 88 | 89 | for it.Seek(dbUserPrefix); it.ValidForPrefix(dbUserPrefix); it.Next() { 90 | key := it.Item().Key() 91 | if !bytes.HasSuffix(key, registrationSuffix) { 92 | continue 93 | } 94 | userID := bytes.TrimSuffix(bytes.TrimPrefix(key, dbUserPrefix), registrationSuffix) 95 | clone := new([64]byte) 96 | copy(clone[:], userID) 97 | users = append(users, clone) 98 | } 99 | return nil 100 | }) 101 | if err != nil { 102 | return nil, err 103 | } 104 | return users, nil 105 | } 106 | 107 | func (srv *Server) userFilterHandler(w http.ResponseWriter, req *http.Request) { 108 | if !srv.authorized(srv.registrarKey, w, req) { 109 | return 110 | } 111 | 112 | usernames, err := srv.RegisteredUsernames() 113 | if err != nil { 114 | http.Error(w, err.Error(), http.StatusInternalServerError) 115 | return 116 | } 117 | // Leave some room in the bloom filter so the registrar can add its own usernames. 118 | f := bloom.New(bloom.Optimal(len(usernames)+1000, 0.0001)) 119 | for _, username := range usernames { 120 | f.Set(username[:]) 121 | } 122 | data, err := json.Marshal(f) 123 | if err != nil { 124 | panic(err) 125 | } 126 | w.Write(data) 127 | } 128 | -------------------------------------------------------------------------------- /pkg/register.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "encoding/json" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/davidlazar/go-crypto/encoding/base32" 15 | "github.com/dgraph-io/badger" 16 | 17 | "vuvuzela.io/alpenhorn/log" 18 | ) 19 | 20 | type registerArgs struct { 21 | Username string 22 | 23 | // LoginKey is how clients authenticate to the PKG server. 24 | LoginKey ed25519.PublicKey 25 | 26 | // RegistrationToken can be used to authenticate registrations. 27 | RegistrationToken string 28 | } 29 | 30 | func (srv *Server) registerHandler(w http.ResponseWriter, req *http.Request) { 31 | body := http.MaxBytesReader(w, req.Body, 256) 32 | args := new(registerArgs) 33 | err := json.NewDecoder(body).Decode(args) 34 | if err != nil { 35 | httpError(w, errorf(ErrBadRequestJSON, "%s", err)) 36 | return 37 | } 38 | 39 | logger := srv.log.WithFields(log.Fields{"username": args.Username, "loginKey": base32.EncodeToString(args.LoginKey)}) 40 | err = srv.register(args) 41 | if err != nil { 42 | logger = logger.WithFields(log.Fields{"code": errorCode(err).String()}) 43 | if isInternalError(err) { 44 | logger.Errorf("Registration failed: %s", err) 45 | } else { 46 | // Avoid polluting stderr for user-caused errors. 47 | logger.Infof("Registration failed: %s", err) 48 | } 49 | httpError(w, err) 50 | return 51 | } 52 | logger.Info("Registration successful") 53 | 54 | // reply with valid json 55 | w.Write([]byte("\"OK\"")) 56 | } 57 | 58 | func (srv *Server) register(args *registerArgs) error { 59 | id, err := UsernameToIdentity(args.Username) 60 | if err != nil { 61 | return errorf(ErrInvalidUsername, "%s", err) 62 | } 63 | if len(args.LoginKey) != ed25519.PublicKeySize { 64 | return errorf(ErrInvalidLoginKey, "got %d bytes, want %d bytes", len(args.LoginKey), ed25519.PublicKeySize) 65 | } 66 | 67 | err = srv.regTokenHandler(args.Username, args.RegistrationToken) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | tx := srv.db.NewTransaction(true) 73 | defer tx.Discard() 74 | 75 | key := dbUserKey(id, registrationSuffix) 76 | _, err = tx.Get(key) 77 | if err != nil && err != badger.ErrKeyNotFound { 78 | return errorf(ErrDatabaseError, "%s", err) 79 | } 80 | if err == nil { 81 | return errorf(ErrAlreadyRegistered, "%q", args.Username) 82 | } 83 | 84 | newUser := userState{ 85 | LoginKey: args.LoginKey, 86 | } 87 | 88 | err = tx.Set(key, newUser.Marshal()) 89 | if err != nil { 90 | return errorf(ErrDatabaseError, "%s", err) 91 | } 92 | 93 | err = appendLog(tx, id, UserEvent{ 94 | Time: time.Now(), 95 | Type: EventRegistered, 96 | LoginKey: args.LoginKey, 97 | }) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | err = tx.Commit() 103 | if err != nil { 104 | return errorf(ErrDatabaseError, "%s", err) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func ExternalVerifier(verifyURL string) RegTokenHandler { 111 | return func(username string, token string) error { 112 | vals := url.Values{ 113 | "username": []string{username}, 114 | "token": []string{token}, 115 | } 116 | resp, err := http.PostForm(verifyURL, vals) 117 | if err != nil { 118 | return err 119 | } 120 | defer resp.Body.Close() 121 | if resp.StatusCode == http.StatusOK { 122 | return nil 123 | } 124 | return errorf(ErrInvalidToken, "") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /cmd/alpenhorn-cdn/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "path/filepath" 16 | "text/template" 17 | 18 | "vuvuzela.io/alpenhorn/cdn" 19 | "vuvuzela.io/alpenhorn/cmd/cmdutil" 20 | "vuvuzela.io/alpenhorn/config" 21 | "vuvuzela.io/alpenhorn/edtls" 22 | "vuvuzela.io/alpenhorn/encoding/toml" 23 | "vuvuzela.io/alpenhorn/internal/alplog" 24 | "vuvuzela.io/alpenhorn/log" 25 | "vuvuzela.io/crypto/rand" 26 | ) 27 | 28 | var ( 29 | doinit = flag.Bool("init", false, "create config file") 30 | persistPath = flag.String("persist", "persist_cdn", "persistent data directory") 31 | ) 32 | 33 | type Config struct { 34 | PublicKey ed25519.PublicKey 35 | PrivateKey ed25519.PrivateKey 36 | 37 | ListenAddr string 38 | } 39 | 40 | var funcMap = template.FuncMap{ 41 | "base32": toml.EncodeBytes, 42 | } 43 | 44 | const confTemplate = `# Alpenhorn CDN config 45 | 46 | publicKey = {{.PublicKey | base32 | printf "%q"}} 47 | privateKey = {{.PrivateKey | base32 | printf "%q"}} 48 | 49 | listenAddr = {{.ListenAddr | printf "%q" }} 50 | ` 51 | 52 | func writeNewConfig(path string) { 53 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | conf := &Config{ 59 | PublicKey: publicKey, 60 | PrivateKey: privateKey, 61 | 62 | ListenAddr: "0.0.0.0:8080", 63 | } 64 | 65 | tmpl := template.Must(template.New("config").Funcs(funcMap).Parse(confTemplate)) 66 | 67 | buf := new(bytes.Buffer) 68 | err = tmpl.Execute(buf, conf) 69 | if err != nil { 70 | log.Fatalf("template error: %s", err) 71 | } 72 | data := buf.Bytes() 73 | 74 | err = ioutil.WriteFile(path, data, 0600) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | fmt.Printf("wrote %s\n", path) 79 | } 80 | 81 | func main() { 82 | flag.Parse() 83 | 84 | if err := os.MkdirAll(*persistPath, 0700); err != nil { 85 | log.Fatal(err) 86 | } 87 | confPath := filepath.Join(*persistPath, "cdn.conf") 88 | 89 | if *doinit { 90 | if cmdutil.Overwrite(confPath) { 91 | writeNewConfig(confPath) 92 | } 93 | return 94 | } 95 | 96 | data, err := ioutil.ReadFile(confPath) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | conf := new(Config) 101 | err = toml.Unmarshal(data, conf) 102 | if err != nil { 103 | log.Fatalf("error parsing config %q: %s", confPath, err) 104 | } 105 | 106 | if conf.ListenAddr == "" { 107 | log.Fatal("empty listen address in config") 108 | } 109 | 110 | logsDir := filepath.Join(*persistPath, "logs") 111 | logHandler, err := alplog.NewProductionOutput(logsDir) 112 | if err != nil { 113 | log.Fatal(err) 114 | } 115 | 116 | signedConfig, err := config.StdClient.CurrentConfig("AddFriend") 117 | if err != nil { 118 | log.Fatal(err) 119 | } 120 | addFriendConfig := signedConfig.Inner.(*config.AddFriendConfig) 121 | 122 | dbPath := filepath.Join(*persistPath, "bolt_db") 123 | server, err := cdn.New(dbPath, addFriendConfig.Coordinator.Key) 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | 128 | listener, err := edtls.Listen("tcp", conf.ListenAddr, conf.PrivateKey) 129 | if err != nil { 130 | log.Fatalf("edtls listen: %s", err) 131 | } 132 | 133 | log.Infof("Listening on %q; logging to %s", conf.ListenAddr, logHandler.Name()) 134 | log.StdLogger.EntryHandler = logHandler 135 | log.Infof("Listening on %q", conf.ListenAddr) 136 | 137 | err = http.Serve(listener, server) 138 | log.Fatalf("Shutdown: %s", err) 139 | } 140 | -------------------------------------------------------------------------------- /keywheel/keywheel_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package keywheel 6 | 7 | import ( 8 | "bytes" 9 | "crypto/rand" 10 | "testing" 11 | ) 12 | 13 | func TestMarshal(t *testing.T) { 14 | var w1 Wheel 15 | w1.Put("alice", 100, new([32]byte)) 16 | k1 := w1.SessionKey("alice", 100) 17 | w1Bytes, _ := w1.MarshalBinary() 18 | 19 | var w2 Wheel 20 | err := w2.UnmarshalBinary(w1Bytes) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | k2 := w2.SessionKey("alice", 100) 25 | 26 | if !bytes.Equal(k1[:], k2[:]) { 27 | t.Fatal("session keys differ after re-opening") 28 | } 29 | 30 | w2.EraseKeys(100) 31 | w2.EraseKeys(101) 32 | 33 | data, _ := w2.MarshalBinary() 34 | expected := `{ 35 | "alice": { 36 | "Round": 102, 37 | "Secret": "bzc1exn1snjc7c43szqhmpd8h7c1hgep42ydwpy48ec6zt02ctx0" 38 | } 39 | } 40 | ` 41 | // ignore version byte 42 | if !bytes.Equal(data[1:], []byte(expected)) { 43 | t.Fatalf("persisted state, got:\n%q\nwant:\n%q\n", data[1:], expected) 44 | } 45 | } 46 | 47 | func TestKeywheel(t *testing.T) { 48 | // Alice's keywheel 49 | alice := "alice@example.org" 50 | var aw Wheel 51 | 52 | // Bob's keywheel 53 | bob := "bob@example.org" 54 | var bw Wheel 55 | 56 | // shared key between Blice and Bob 57 | abKey := new([32]byte) 58 | rand.Read(abKey[:]) 59 | 60 | aw.Put(bob, 100, abKey) 61 | bw.Put(alice, 100, abKey) 62 | 63 | keyA := aw.SessionKey(bob, 100) 64 | keyB := bw.SessionKey(alice, 100) 65 | if keyA == nil { 66 | t.Fatal("got nil session key") 67 | } 68 | if !bytes.Equal(keyA[:], keyB[:]) { 69 | t.Fatalf("%x != %x", keyA[:], keyB[:]) 70 | } 71 | 72 | if key := aw.SessionKey(bob, 90); key != nil { 73 | t.Fatalf("expected nil session key for round 90") 74 | } 75 | 76 | aw.EraseKeys(99) 77 | if key := aw.SessionKey(bob, 100); !bytes.Equal(keyA[:], key[:]) { 78 | t.Fatalf("%x != %x", keyA[:], key[:]) 79 | } 80 | aw.EraseKeys(100) 81 | if key := aw.SessionKey(bob, 100); key != nil { 82 | t.Fatalf("expected nil session key for round 100") 83 | } 84 | if xs := aw.IncomingDialTokens(bob, 100, 5); len(xs) != 0 { 85 | t.Fatalf("expected empty token list for round 100") 86 | } 87 | 88 | keyA = aw.SessionKey(bob, 120) 89 | keyB = bw.SessionKey(alice, 120) 90 | if keyA == nil { 91 | t.Fatal("got nil session key") 92 | } 93 | if !bytes.Equal(keyA[:], keyB[:]) { 94 | t.Fatalf("%x != %x", keyA[:], keyB[:]) 95 | } 96 | 97 | chris := "chris@example.org" 98 | var cw Wheel 99 | 100 | acKey := new([32]byte) 101 | rand.Read(acKey[:]) 102 | aw.Put(chris, 101, acKey) 103 | cw.Put(alice, 101, acKey) 104 | 105 | numIntents := 5 106 | alltokens := aw.IncomingDialTokens(alice, 101, numIntents) 107 | if len(alltokens) != 2 { 108 | t.Fatalf("expected single element in token list, got %d", len(alltokens)) 109 | } 110 | for _, u := range alltokens { 111 | var w *Wheel 112 | if u.FromUsername == bob { 113 | w = &bw 114 | } else if u.FromUsername == chris { 115 | w = &cw 116 | } else { 117 | t.Fatalf("unexpected user in incoming dial tokens: %s", u.FromUsername) 118 | } 119 | for i := 0; i < numIntents; i++ { 120 | tok := w.OutgoingDialToken(alice, 101, i) 121 | if !bytes.Equal(tok[:], u.Tokens[i][:]) { 122 | t.Fatalf("dial token mismatch; user=%s intent=%d: expected %v, got %v", u.FromUsername, i, u.Tokens[i][:], tok[:]) 123 | } 124 | } 125 | } 126 | } 127 | 128 | func BenchmarkGetSecret(b *testing.B) { 129 | rs := &roundSecret{ 130 | Round: 0, 131 | Secret: new([32]byte), 132 | } 133 | for i := 0; i < b.N; i++ { 134 | _ = rs.getSecret(1) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /cdn/cdn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package cdn 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "crypto/rand" 11 | "encoding/gob" 12 | "fmt" 13 | "io/ioutil" 14 | "net" 15 | "net/http" 16 | "os" 17 | "path/filepath" 18 | "testing" 19 | "time" 20 | 21 | "github.com/davidlazar/go-crypto/encoding/base32" 22 | 23 | "vuvuzela.io/alpenhorn/edtls" 24 | ) 25 | 26 | func TestCDN(t *testing.T) { 27 | coordinatorPub, coordinatorPriv, _ := ed25519.GenerateKey(rand.Reader) 28 | cdnPub, cdnPriv, _ := ed25519.GenerateKey(rand.Reader) 29 | 30 | dir, err := ioutil.TempDir("", "TestCDN") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | defer os.RemoveAll(dir) 35 | 36 | defaultTTL = 1 * time.Second 37 | deleteExpiredTickRate = 1 * time.Second 38 | 39 | dbPath := filepath.Join(dir, "cdn.db") 40 | cdn, err := New(dbPath, coordinatorPub) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | listener, err := edtls.Listen("tcp", "127.0.0.1:8080", cdnPriv) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | go http.Serve(listener, cdn) 50 | 51 | data := make(map[string][]byte) 52 | data["1"] = []byte("hello") 53 | data["2"] = []byte("world") 54 | 55 | buf := new(bytes.Buffer) 56 | if err := gob.NewEncoder(buf).Encode(data); err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | { 61 | client := &http.Client{ 62 | Transport: &http.Transport{ 63 | DialTLS: func(network, addr string) (net.Conn, error) { 64 | return edtls.Dial(network, addr, cdnPub, coordinatorPriv) 65 | }, 66 | }, 67 | } 68 | nbURL := fmt.Sprintf("https://%s/newbucket?bucket=%s&uploader=%s", "127.0.0.1:8080", "foo/42", base32.EncodeToString(coordinatorPub)) 69 | resp, err := client.Post(nbURL, "", nil) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | if resp.StatusCode != http.StatusOK { 74 | msg, _ := ioutil.ReadAll(resp.Body) 75 | t.Fatalf("newbucket failed: %s: %s", resp.Status, msg) 76 | } 77 | resp, err = client.Post("https://127.0.0.1:8080/put?bucket=foo/42", "", buf) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | defer resp.Body.Close() 82 | body, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | if resp.StatusCode != http.StatusOK { 87 | t.Fatalf("bad response status: %s; body = %q", resp.Status, body) 88 | } 89 | } 90 | 91 | { 92 | client := &http.Client{ 93 | Transport: &http.Transport{ 94 | DialTLS: func(network, addr string) (net.Conn, error) { 95 | return edtls.Dial(network, addr, cdnPub, nil) 96 | }, 97 | }, 98 | } 99 | resp, err := client.Get("https://127.0.0.1:8080/get?bucket=foo/42&key=2") 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | defer resp.Body.Close() 104 | body, err := ioutil.ReadAll(resp.Body) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | if resp.StatusCode != http.StatusOK { 109 | t.Fatalf("bad response status: %s; body = %q", resp.Status, body) 110 | } 111 | if !bytes.Equal(body, data["2"]) { 112 | t.Fatalf("got %q, want %q", body, data["2"]) 113 | } 114 | } 115 | 116 | { 117 | time.Sleep(2 * time.Second) 118 | client := &http.Client{ 119 | Transport: &http.Transport{ 120 | DialTLS: func(network, addr string) (net.Conn, error) { 121 | return edtls.Dial(network, addr, cdnPub, nil) 122 | }, 123 | }, 124 | } 125 | resp, err := client.Get("https://127.0.0.1:8080/get?bucket=foo/42&key=2") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | defer resp.Body.Close() 130 | if resp.StatusCode != http.StatusNotFound { 131 | t.Fatalf("expected 404 not found, got %s", resp.Status) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /config/server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "crypto/rand" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "testing" 16 | "time" 17 | 18 | "github.com/davidlazar/go-crypto/encoding/base32" 19 | ) 20 | 21 | func TestServer(t *testing.T) { 22 | tmpDir, err := ioutil.TempDir("", "alpenhorn_config_test") 23 | if err != nil { 24 | panic(err) 25 | } 26 | defer os.RemoveAll(tmpDir) 27 | persistPath := filepath.Join(tmpDir, "config-server-state") 28 | 29 | guardian1Public, guardian1Private, err := ed25519.GenerateKey(rand.Reader) 30 | if err != nil { 31 | panic(err) 32 | } 33 | _ = guardian1Private 34 | 35 | startingConfig := &SignedConfig{ 36 | Version: 1, 37 | 38 | Created: time.Now(), 39 | Expires: time.Now().Add(24 * time.Hour), 40 | 41 | Service: "AddFriend", 42 | Inner: &AddFriendConfig{ 43 | Version: 1, 44 | 45 | Coordinator: CoordinatorConfig{ 46 | Key: guardian1Public, 47 | Address: "localhost:1234", 48 | }, 49 | CDNServer: CDNServerConfig{ 50 | Address: "localhost:8080", 51 | Key: guardian1Public, 52 | }, 53 | }, 54 | 55 | Guardians: []Guardian{ 56 | { 57 | Username: "guardian1", 58 | Key: guardian1Public, 59 | }, 60 | }, 61 | } 62 | 63 | server, err := CreateServer(persistPath) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | err = server.SetCurrentConfig(startingConfig) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | server, err = LoadServer(persistPath) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | _, currHash := server.CurrentConfig("AddFriend") 78 | if currHash != startingConfig.Hash() { 79 | t.Fatal("wrong current config hash") 80 | } 81 | 82 | listener, err := net.Listen("tcp", "localhost:0") 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | go func() { 87 | err := http.Serve(listener, server) 88 | if err != http.ErrServerClosed { 89 | t.Fatal(err) 90 | } 91 | }() 92 | client := &Client{ 93 | ConfigServerURL: "http://" + listener.Addr().String(), 94 | } 95 | 96 | newConfig := &SignedConfig{ 97 | Version: 1, 98 | 99 | Created: time.Now(), 100 | Expires: time.Now().Add(24 * time.Hour), 101 | 102 | PrevConfigHash: startingConfig.Hash(), 103 | 104 | Service: "AddFriend", 105 | Inner: &AddFriendConfig{ 106 | Version: 1, 107 | 108 | Coordinator: CoordinatorConfig{ 109 | Key: guardian1Public, 110 | Address: "localhost:1234", 111 | }, 112 | CDNServer: CDNServerConfig{ 113 | Address: "localhost:8081", 114 | Key: guardian1Public, 115 | }, 116 | }, 117 | } 118 | 119 | { 120 | // Try uploading a new config without the guardian's signature. 121 | err := client.SetCurrentConfig(newConfig) 122 | if err == nil { 123 | t.Fatal("expecting error") 124 | } 125 | } 126 | 127 | // Sign the new config and try again. 128 | newConfig.Signatures = make(map[string][]byte) 129 | gk := base32.EncodeToString(guardian1Public) 130 | newConfig.Signatures[gk] = ed25519.Sign(guardian1Private, newConfig.SigningMessage()) 131 | 132 | { 133 | err := client.SetCurrentConfig(newConfig) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | } 138 | 139 | { 140 | conf, err := client.CurrentConfig("AddFriend") 141 | if err != nil { 142 | t.Fatal(err) 143 | } 144 | 145 | if conf.Hash() != newConfig.Hash() { 146 | t.Fatalf("bad response config: got %q, want %q", conf.Hash(), newConfig.Hash()) 147 | } 148 | } 149 | 150 | { 151 | chain, err := client.FetchAndVerifyChain(startingConfig, newConfig.Hash()) 152 | if err != nil { 153 | t.Fatal(err) 154 | } 155 | 156 | if chain[0].Hash() != newConfig.Hash() { 157 | t.Fatal("wrong config in chain") 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /encoding/toml/parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package toml 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | const example1 string = ` 13 | foo = ["hello\tworld"] 14 | bar = 0 15 | 16 | [servers] 17 | a = [1,-19,3] 18 | 19 | [servers.alpha] # server settings 20 | ip = "10.0.0.1" 21 | num = 42 22 | float = 1.2345 23 | 24 | [servers.beta] 25 | ip = "10.0.0.2" 26 | num = 23 27 | float = -90.73 28 | ` 29 | 30 | var example1Result = map[string]interface{}{ 31 | "foo": []interface{}{"hello\tworld"}, 32 | "bar": int64(0), 33 | "servers": map[string]interface{}{ 34 | "a": []interface{}{int64(1), int64(-19), int64(3)}, 35 | "alpha": map[string]interface{}{"ip": "10.0.0.1", "num": int64(42), "float": float64(1.2345)}, 36 | "beta": map[string]interface{}{"ip": "10.0.0.2", "num": int64(23), "float": float64(-90.73)}, 37 | }, 38 | } 39 | 40 | const example2 string = ` 41 | [servers] 42 | 43 | [servers.alpha] 44 | b = true 45 | 46 | [servers.beta] 47 | b = false 48 | ` 49 | 50 | var example2Result = map[string]interface{}{ 51 | "servers": map[string]interface{}{ 52 | "alpha": map[string]interface{}{"b": true}, 53 | "beta": map[string]interface{}{"b": false}, 54 | }, 55 | } 56 | 57 | const example3 string = ` 58 | [[products]] 59 | name = "Hammer" 60 | sku = 738594937 61 | 62 | [[products]] 63 | 64 | [[products]] 65 | name = "Nail" 66 | sku = 284758393 67 | color = "gray" 68 | ` 69 | 70 | var example3Result = map[string]interface{}{ 71 | "products": []map[string]interface{}{ 72 | {"name": "Hammer", "sku": int64(738594937)}, 73 | {}, 74 | {"name": "Nail", "sku": int64(284758393), "color": "gray"}, 75 | }, 76 | } 77 | 78 | var example4 = ` 79 | [[fruit]] 80 | name = "apple" 81 | 82 | [fruit.physical] 83 | color = "red" 84 | shape = "round" 85 | 86 | [[fruit.variety]] 87 | name = "red delicious" 88 | 89 | [[fruit.variety]] 90 | name = "granny smith" 91 | 92 | [[fruit]] 93 | name = "banana" 94 | 95 | [[fruit.variety]] 96 | name = "plantain" 97 | ` 98 | 99 | var example4Result = map[string]interface{}{ 100 | "fruit": []map[string]interface{}{ 101 | { 102 | "name": "apple", 103 | "physical": map[string]interface{}{ 104 | "color": "red", 105 | "shape": "round", 106 | }, 107 | "variety": []map[string]interface{}{ 108 | {"name": "red delicious"}, 109 | {"name": "granny smith"}, 110 | }, 111 | }, 112 | { 113 | "name": "banana", 114 | "variety": []map[string]interface{}{ 115 | {"name": "plantain"}, 116 | }, 117 | }, 118 | }, 119 | } 120 | 121 | func shouldParse(t *testing.T, name string, input string, expected interface{}) { 122 | actual, err := parse(input) 123 | if err != nil { 124 | t.Fatalf("toml parse error: %s: %s", name, err) 125 | } 126 | 127 | if !reflect.DeepEqual(actual, expected) { 128 | t.Fatalf("unexpected parse result for %s:\ngot:\n%#v\nwant:\n%#v\n", name, actual, expected) 129 | } 130 | } 131 | 132 | func TestParse(t *testing.T) { 133 | shouldParse(t, "example1", example1, example1Result) 134 | shouldParse(t, "example2", example2, example2Result) 135 | shouldParse(t, "example3", example3, example3Result) 136 | shouldParse(t, "example4", example4, example4Result) 137 | } 138 | 139 | var badExample1 = ` 140 | [[foo.bar]] 141 | x = 123 142 | 143 | [[foo]] 144 | y = 888 145 | 146 | [[foo]] 147 | y = 999 148 | ` 149 | 150 | var badExample2 = ` 151 | # INVALID TOML DOC 152 | [[fruit]] 153 | name = "apple" 154 | 155 | [[fruit.variety]] 156 | name = "red delicious" 157 | 158 | # This table conflicts with the previous table 159 | [fruit.variety] 160 | name = "granny smith" 161 | ` 162 | 163 | func shouldNotParse(t *testing.T, name string, input string) { 164 | actual, err := parse(input) 165 | if err == nil { 166 | t.Fatalf("expected parse to fail for %s, got:\n%#v\n", name, actual) 167 | } 168 | } 169 | 170 | func TestNoParse(t *testing.T) { 171 | shouldNotParse(t, "badExample1", badExample1) 172 | shouldNotParse(t, "badExample2", badExample2) 173 | } 174 | -------------------------------------------------------------------------------- /encoding/toml/parser.y: -------------------------------------------------------------------------------- 1 | %{ 2 | package toml 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type entry struct { 11 | key string 12 | val interface{} 13 | } 14 | 15 | type table struct { 16 | isArray bool 17 | keys []string 18 | entries map[string]interface{} 19 | } 20 | %} 21 | 22 | %union { 23 | str string 24 | line int 25 | entries map[string]interface{} 26 | entry entry 27 | value interface{} 28 | values []interface{} 29 | table table 30 | tables map[string]interface{} 31 | keys []string 32 | } 33 | 34 | %token itemError 35 | %token itemBool 36 | %token itemKey 37 | %token itemLeftBracket 38 | %token itemLeftDoubleBracket 39 | %token itemRightBracket 40 | %token itemRightDoubleBracket 41 | %token itemComma 42 | %token itemEqual 43 | %token itemNumber 44 | %token itemString 45 | 46 | %type entry 47 | %type entries 48 | %type value 49 | %type values 50 | %type tables 51 | %type table 52 | %type keys 53 | 54 | %% 55 | 56 | top 57 | : entries tables { 58 | for k, v := range $1 { 59 | $2[k] = v 60 | } 61 | yylex.(*lexer).result = $2 62 | } 63 | 64 | tables 65 | : /**/ { $$ = make(map[string]interface{}) } 66 | | tables table { 67 | m := $1 68 | for i, key := range $2.keys { 69 | v, ok := m[key] 70 | if !ok { 71 | if $2.isArray && i == len($2.keys)-1 { 72 | array := make([]map[string]interface{}, 1) 73 | array[0] = make(map[string]interface{}) 74 | m[key] = array 75 | m = array[0] 76 | continue 77 | } 78 | nestedMap := make(map[string]interface{}) 79 | m[key] = nestedMap 80 | m = nestedMap 81 | continue 82 | } 83 | switch t := v.(type) { 84 | case map[string]interface{}: 85 | if $2.isArray && i == len($2.keys)-1 { 86 | yylex.Error(fmt.Sprintf("key %q used as array but previously defined as map", key)) 87 | return 1 88 | } 89 | m = t 90 | case []map[string]interface{}: 91 | if $2.isArray && i == len($2.keys)-1 { 92 | arrayElement := make(map[string]interface{}) 93 | t = append(t, arrayElement) 94 | m[key] = t 95 | m = arrayElement 96 | } else if !$2.isArray && i == len($2.keys)-1 { 97 | yylex.Error(fmt.Sprintf("key %q used as map but previously defined as array", key)) 98 | return 1 99 | } else { 100 | m = t[len(t)-1] 101 | } 102 | default: 103 | yylex.Error(fmt.Sprintf("key %q already defined as non-map value", key)) 104 | return 1 105 | } 106 | } 107 | for k, v := range $2.entries { 108 | m[k] = v 109 | } 110 | } 111 | 112 | table 113 | : itemLeftBracket keys itemRightBracket entries { 114 | $$ = table{ 115 | isArray: false, 116 | keys: $2, 117 | entries: $4, 118 | } 119 | } 120 | | itemLeftDoubleBracket keys itemRightDoubleBracket entries { 121 | $$ = table{ 122 | isArray: true, 123 | keys: $2, 124 | entries: $4, 125 | } 126 | } 127 | 128 | keys 129 | : itemKey { $$ = []string{$1} } 130 | | keys itemKey { $$ = append($1, $2) } 131 | 132 | entries 133 | : /**/ { $$ = map[string]interface{}{} } 134 | | entries entry { $1[$2.key] = $2.val; $$ = $1 } 135 | 136 | entry 137 | : itemKey itemEqual value { $$ = entry{$1, $3} } 138 | 139 | value 140 | : itemBool { if $1 == "true" { $$ = true } else { $$ = false } } 141 | | itemNumber { 142 | if strings.Contains($1, ".") { 143 | n, err := strconv.ParseFloat($1, 64) 144 | if err != nil { 145 | yylex.Error(fmt.Sprintf("error parsing float: %s", err)) 146 | return 1 147 | } 148 | $$ = n 149 | } else { 150 | n, err := strconv.ParseInt($1, 10, 64) 151 | if err != nil { 152 | yylex.Error(fmt.Sprintf("error parsing int: %s", err)) 153 | return 1 154 | } 155 | $$ = n 156 | } 157 | } 158 | | itemString { 159 | s, err := strconv.Unquote($1) 160 | if err != nil { 161 | yylex.Error(fmt.Sprintf("error parsing string: %s", err)) 162 | return 1 163 | } 164 | $$ = s 165 | } 166 | | itemLeftBracket values itemRightBracket { $$ = $2 } 167 | 168 | values 169 | : value { $$ = []interface{}{$1} } 170 | | values itemComma value { $$ = append($1, $3) } 171 | -------------------------------------------------------------------------------- /config/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "time" 14 | 15 | "vuvuzela.io/alpenhorn/errors" 16 | "vuvuzela.io/internal/debug" 17 | ) 18 | 19 | var StdClient = &Client{ 20 | ConfigServerURL: "https://configs.vuvuzela.io", 21 | } 22 | 23 | type Client struct { 24 | ConfigServerURL string 25 | } 26 | 27 | var httpClient = &http.Client{ 28 | Timeout: 10 * time.Second, 29 | } 30 | 31 | func (c *Client) CurrentConfig(service string) (*SignedConfig, error) { 32 | url := fmt.Sprintf("%s/current?service=%s", c.ConfigServerURL, service) 33 | resp, err := httpClient.Get(url) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer resp.Body.Close() 38 | 39 | if resp.StatusCode != http.StatusOK { 40 | msg, _ := ioutil.ReadAll(resp.Body) 41 | return nil, errors.New("Get %q: %s: %q", url, resp.Status, msg) 42 | } 43 | 44 | var config *SignedConfig 45 | if err := json.NewDecoder(resp.Body).Decode(&config); err != nil { 46 | return nil, errors.Wrap(err, "unmarshaling config") 47 | } 48 | 49 | if err := config.Validate(); err != nil { 50 | return nil, err 51 | } 52 | if config.Service != service { 53 | return nil, errors.New("received config for wrong service type: want %q, got %q", service, config.Service) 54 | } 55 | if time.Now().After(config.Expires) { 56 | return nil, errors.New("config expired on %s", config.Expires) 57 | } 58 | if err := config.Verify(); err != nil { 59 | return nil, err 60 | } 61 | 62 | return config, nil 63 | } 64 | 65 | // FetchAndVerifyChain fetches and verifies a config chain starting with 66 | // the have config and ending with the want config. The chain is returned 67 | // in reverse order so chain[0].Hash() = want and chain[len(chain)-1] = have. 68 | func (c *Client) FetchAndVerifyChain(have *SignedConfig, want string) ([]*SignedConfig, error) { 69 | url := fmt.Sprintf("%s/getchain?have=%s&want=%s", c.ConfigServerURL, have.Hash(), want) 70 | resp, err := http.Get(url) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer resp.Body.Close() 75 | 76 | if resp.StatusCode != http.StatusOK { 77 | msg, _ := ioutil.ReadAll(resp.Body) 78 | return nil, errors.New("Get %q: %s: %q", url, resp.Status, msg) 79 | } 80 | 81 | var configs []*SignedConfig 82 | if err := json.NewDecoder(resp.Body).Decode(&configs); err != nil { 83 | return nil, errors.Wrap(err, "unmarshaling configs") 84 | } 85 | if len(configs) == 0 { 86 | return nil, errors.New("no configs returned from server") 87 | } 88 | 89 | newConfig := configs[0] 90 | if err := newConfig.Validate(); err != nil { 91 | return nil, err 92 | } 93 | if newConfig.Hash() != want { 94 | return nil, errors.New("received config with wrong hash: want %q, got %q\n->%s\n", want, newConfig.Hash(), debug.Pretty(newConfig)) 95 | } 96 | if newConfig.Service != have.Service { 97 | return nil, errors.New("received config for wrong service type: want %q, got %q", have.Service, newConfig.Service) 98 | } 99 | if !newConfig.Created.After(have.Created) { 100 | return nil, errors.New("new config not created after prev config: prev=%s next=%s", have.Hash(), newConfig.Hash()) 101 | } 102 | if time.Now().After(newConfig.Expires) { 103 | return nil, errors.New("config expired on %s", newConfig.Expires) 104 | } 105 | 106 | configs = append(configs, have) 107 | err = VerifyConfigChain(configs...) 108 | if err != nil { 109 | return nil, errors.Wrap(err, "failed to verify new config") 110 | } 111 | 112 | return configs, nil 113 | } 114 | 115 | func (c *Client) SetCurrentConfig(conf *SignedConfig) error { 116 | body := new(bytes.Buffer) 117 | err := json.NewEncoder(body).Encode(conf) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | url := fmt.Sprintf("%s/new", c.ConfigServerURL) 123 | resp, err := http.Post(url, "application/json", body) 124 | if err != nil { 125 | return err 126 | } 127 | defer resp.Body.Close() 128 | 129 | if resp.StatusCode != http.StatusOK { 130 | msg, _ := ioutil.ReadAll(resp.Body) 131 | return errors.New("error setting %q config: %s: %q", conf.Service, resp.Status, msg) 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /cmd/alpenhorn-mixer/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "net" 14 | "os" 15 | "path/filepath" 16 | "text/template" 17 | 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/credentials" 20 | 21 | "vuvuzela.io/alpenhorn/addfriend" 22 | "vuvuzela.io/alpenhorn/cmd/cmdutil" 23 | "vuvuzela.io/alpenhorn/config" 24 | "vuvuzela.io/alpenhorn/dialing" 25 | "vuvuzela.io/alpenhorn/edtls" 26 | "vuvuzela.io/alpenhorn/encoding/toml" 27 | "vuvuzela.io/alpenhorn/log" 28 | "vuvuzela.io/crypto/rand" 29 | "vuvuzela.io/vuvuzela/mixnet" 30 | pb "vuvuzela.io/vuvuzela/mixnet/convopb" 31 | ) 32 | 33 | var ( 34 | doinit = flag.Bool("init", false, "create config file") 35 | persistPath = flag.String("persist", "persist_alpmix", "persistent data directory") 36 | ) 37 | 38 | type Config struct { 39 | PublicKey ed25519.PublicKey 40 | PrivateKey ed25519.PrivateKey 41 | 42 | ListenAddr string 43 | 44 | AddFriendNoise rand.Laplace 45 | DialingNoise rand.Laplace 46 | } 47 | 48 | var funcMap = template.FuncMap{ 49 | "base32": toml.EncodeBytes, 50 | } 51 | 52 | const confTemplate = `# Alpenhorn mixnet server config 53 | 54 | publicKey = {{.PublicKey | base32 | printf "%q"}} 55 | privateKey = {{.PrivateKey | base32 | printf "%q"}} 56 | 57 | listenAddr = {{.ListenAddr | printf "%q"}} 58 | 59 | [addFriendNoise] 60 | mu = {{.AddFriendNoise.Mu | printf "%0.1f"}} 61 | b = {{.AddFriendNoise.B | printf "%0.1f"}} 62 | 63 | [dialingNoise] 64 | mu = {{.DialingNoise.Mu | printf "%0.1f"}} 65 | b = {{.DialingNoise.B | printf "%0.1f"}} 66 | ` 67 | 68 | func writeNewConfig(path string) { 69 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | conf := &Config{ 75 | ListenAddr: "0.0.0.0:28000", 76 | PublicKey: publicKey, 77 | PrivateKey: privateKey, 78 | 79 | AddFriendNoise: rand.Laplace{ 80 | Mu: 100, 81 | B: 3.0, 82 | }, 83 | 84 | DialingNoise: rand.Laplace{ 85 | Mu: 100, 86 | B: 3.0, 87 | }, 88 | } 89 | 90 | tmpl := template.Must(template.New("config").Funcs(funcMap).Parse(confTemplate)) 91 | 92 | buf := new(bytes.Buffer) 93 | err = tmpl.Execute(buf, conf) 94 | if err != nil { 95 | log.Fatalf("template error: %s", err) 96 | } 97 | data := buf.Bytes() 98 | 99 | err = ioutil.WriteFile(path, data, 0600) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | fmt.Printf("wrote %s\n", path) 104 | } 105 | 106 | func main() { 107 | flag.Parse() 108 | 109 | if err := os.MkdirAll(*persistPath, 0700); err != nil { 110 | log.Fatal(err) 111 | } 112 | confPath := filepath.Join(*persistPath, "mixer.conf") 113 | 114 | if *doinit { 115 | if cmdutil.Overwrite(confPath) { 116 | writeNewConfig(confPath) 117 | } 118 | return 119 | } 120 | 121 | data, err := ioutil.ReadFile(confPath) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | conf := new(Config) 126 | err = toml.Unmarshal(data, conf) 127 | if err != nil { 128 | log.Fatalf("error parsing config %q: %s", confPath, err) 129 | } 130 | 131 | signedConfig, err := config.StdClient.CurrentConfig("AddFriend") 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | addFriendConfig := signedConfig.Inner.(*config.AddFriendConfig) 136 | 137 | mixServer := &mixnet.Server{ 138 | SigningKey: conf.PrivateKey, 139 | // Assumes that AddFriend and Dialing use the same coordinator. 140 | CoordinatorKey: addFriendConfig.Coordinator.Key, 141 | 142 | Services: map[string]mixnet.MixService{ 143 | "AddFriend": &addfriend.Mixer{ 144 | SigningKey: conf.PrivateKey, 145 | Laplace: conf.AddFriendNoise, 146 | }, 147 | 148 | "Dialing": &dialing.Mixer{ 149 | SigningKey: conf.PrivateKey, 150 | Laplace: conf.DialingNoise, 151 | }, 152 | }, 153 | } 154 | 155 | creds := credentials.NewTLS(edtls.NewTLSServerConfig(conf.PrivateKey)) 156 | grpcServer := grpc.NewServer(grpc.Creds(creds)) 157 | 158 | pb.RegisterMixnetServer(grpcServer, mixServer) 159 | 160 | log.Infof("Listening on %q", conf.ListenAddr) 161 | 162 | listener, err := net.Listen("tcp", conf.ListenAddr) 163 | if err != nil { 164 | log.Fatalf("net.Listen: %s", err) 165 | } 166 | 167 | err = grpcServer.Serve(listener) 168 | log.Fatalf("Shutdown: %s", err) 169 | } 170 | -------------------------------------------------------------------------------- /pkg/data.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "encoding/binary" 10 | "encoding/json" 11 | "time" 12 | 13 | "github.com/dgraph-io/badger" 14 | 15 | "vuvuzela.io/alpenhorn/errors" 16 | ) 17 | 18 | var ( 19 | dbUserPrefix = []byte("user:") 20 | registrationSuffix = []byte(":registration") 21 | lastExtractionSuffix = []byte(":lastextract") 22 | userLogSuffix = []byte(":log") 23 | ) 24 | 25 | func dbUserKey(identity *[64]byte, suffix []byte) []byte { 26 | return append(append(dbUserPrefix, identity[:]...), suffix...) 27 | } 28 | 29 | type userState struct { 30 | LoginKey ed25519.PublicKey 31 | } 32 | 33 | const userStateBinaryVersion byte = 1 34 | 35 | func (u userState) Marshal() []byte { 36 | data := make([]byte, 1+ed25519.PublicKeySize) 37 | data[0] = userStateBinaryVersion 38 | copy(data[1:], u.LoginKey) 39 | 40 | return data 41 | } 42 | 43 | func (u *userState) Unmarshal(data []byte) error { 44 | if len(data) < 33 { 45 | return errors.New("short data: got %d bytes", len(data)) 46 | } 47 | if data[0] != userStateBinaryVersion { 48 | return errors.New("userStateBinaryVersion mismatch: got %v, want %v", data[0], userStateBinaryVersion) 49 | } 50 | u.LoginKey = make(ed25519.PublicKey, ed25519.PublicKeySize) 51 | copy(u.LoginKey, data[1:]) 52 | 53 | return nil 54 | } 55 | 56 | type lastExtraction struct { 57 | Round uint32 58 | UnixTime int64 59 | } 60 | 61 | const lastExtractionBinaryVersion byte = 1 62 | 63 | func (e lastExtraction) size() int { 64 | return 1 + 4 + 8 65 | } 66 | 67 | func (e lastExtraction) Marshal() []byte { 68 | data := make([]byte, e.size()) 69 | data[0] = lastExtractionBinaryVersion 70 | binary.BigEndian.PutUint32(data[1:5], e.Round) 71 | binary.BigEndian.PutUint64(data[5:], uint64(e.UnixTime)) 72 | return data 73 | } 74 | 75 | func (e *lastExtraction) Unmarshal(data []byte) error { 76 | if len(data) != e.size() { 77 | return errors.New("bad data length: got %d, want %d", len(data), e.size()) 78 | } 79 | if data[0] != lastExtractionBinaryVersion { 80 | return errors.New("unexpected binary version: %v", data[0]) 81 | } 82 | e.Round = binary.BigEndian.Uint32(data[1:5]) 83 | e.UnixTime = int64(binary.BigEndian.Uint64(data[5:])) 84 | return nil 85 | } 86 | 87 | // A UserEventLog contains the major updates to a user's account. 88 | type UserEventLog []UserEvent 89 | 90 | const userEventLogBinaryVersion byte = 1 91 | 92 | type UserEventType int 93 | 94 | const ( 95 | EventRegistered UserEventType = iota + 1 96 | ) 97 | 98 | type UserEvent struct { 99 | Time time.Time 100 | Type UserEventType 101 | LoginKey ed25519.PublicKey 102 | } 103 | 104 | func (e UserEventLog) Marshal() []byte { 105 | data, err := json.Marshal(e) 106 | if err != nil { 107 | panic(err) 108 | } 109 | return append([]byte{userEventLogBinaryVersion}, data...) 110 | } 111 | 112 | func (e *UserEventLog) Unmarshal(data []byte) error { 113 | if len(data) < 3 { 114 | return errors.New("short data") 115 | } 116 | if data[0] != userEventLogBinaryVersion { 117 | return errors.New("userEventLogBinaryVersion mismatch: got %v, want %v", data[0], userEventLogBinaryVersion) 118 | } 119 | return json.Unmarshal(data[1:], e) 120 | } 121 | 122 | func appendLog(tx *badger.Txn, identity *[64]byte, event UserEvent) error { 123 | logKey := dbUserKey(identity, userLogSuffix) 124 | item, err := tx.Get(logKey) 125 | var currLog UserEventLog 126 | if err == badger.ErrKeyNotFound { 127 | currLog = nil 128 | } else if err != nil { 129 | return errorf(ErrDatabaseError, "%s", err) 130 | } else { 131 | err := item.Value(func(data []byte) error { 132 | return json.Unmarshal(data, currLog) 133 | }) 134 | if err != nil { 135 | return errorf(ErrDatabaseError, "%s", err) 136 | } 137 | } 138 | 139 | currLog = append(currLog, event) 140 | data := currLog.Marshal() 141 | if err := tx.Set(logKey, data); err != nil { 142 | return errorf(ErrDatabaseError, "%s", err) 143 | } 144 | return nil 145 | } 146 | 147 | func (srv *Server) GetUserLog(identity *[64]byte) (UserEventLog, error) { 148 | var log UserEventLog 149 | err := srv.db.View(func(tx *badger.Txn) error { 150 | item, err := tx.Get(dbUserKey(identity, userLogSuffix)) 151 | if err != nil { 152 | return err 153 | } 154 | err = item.Value(func(data []byte) error { 155 | return log.Unmarshal(data) 156 | }) 157 | return err 158 | }) 159 | return log, err 160 | } 161 | -------------------------------------------------------------------------------- /log/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package log_test 6 | 7 | import ( 8 | "crypto/rand" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "testing" 15 | "time" 16 | 17 | "github.com/sirupsen/logrus" 18 | 19 | "vuvuzela.io/alpenhorn/log" 20 | ) 21 | 22 | var ( 23 | fakeMessage = "This is a simulated log message..." 24 | fakeDuration = 12345 * time.Second 25 | fakeError = errors.New("something failed") 26 | ) 27 | 28 | func newJSONLogger(out io.Writer) *log.Logger { 29 | return &log.Logger{ 30 | EntryHandler: log.OutputJSON(log.NewMutexWriter(out)), 31 | Level: log.DebugLevel, 32 | } 33 | } 34 | 35 | func newTextLogger(out io.Writer) *log.Logger { 36 | return &log.Logger{ 37 | EntryHandler: &log.OutputText{ 38 | Out: log.NewMutexWriter(out), 39 | }, 40 | Level: log.DebugLevel, 41 | } 42 | } 43 | 44 | func fakeFields() log.Fields { 45 | return log.Fields{ 46 | "rpc": "NewRound", 47 | "addr": "127.0.0.1:8080", 48 | "duration": fakeDuration, 49 | "error": fakeError, 50 | "round": 876543, 51 | } 52 | } 53 | 54 | func newJSONLogrus(out io.Writer) *logrus.Logger { 55 | return &logrus.Logger{ 56 | Out: out, 57 | Formatter: new(logrus.JSONFormatter), 58 | Hooks: make(logrus.LevelHooks), 59 | Level: logrus.DebugLevel, 60 | } 61 | } 62 | 63 | func newTextLogrus(out io.Writer) *logrus.Logger { 64 | return &logrus.Logger{ 65 | Out: out, 66 | Formatter: new(logrus.TextFormatter), 67 | Hooks: make(logrus.LevelHooks), 68 | Level: logrus.DebugLevel, 69 | } 70 | } 71 | 72 | func fakeLogrusFields() logrus.Fields { 73 | return logrus.Fields{ 74 | "rpc": "NewRound", 75 | "addr": "127.0.0.1:8080", 76 | "duration": fakeDuration, 77 | "error": fakeError, 78 | "round": 876543, 79 | } 80 | } 81 | 82 | func BenchmarkJSONLogrus(b *testing.B) { 83 | logger := newJSONLogrus(ioutil.Discard) 84 | b.ResetTimer() 85 | b.RunParallel(func(pb *testing.PB) { 86 | for pb.Next() { 87 | logger.WithFields(fakeLogrusFields()).Info(fakeMessage) 88 | } 89 | }) 90 | } 91 | 92 | func BenchmarkJSONLog(b *testing.B) { 93 | logger := newJSONLogger(ioutil.Discard) 94 | b.ResetTimer() 95 | b.RunParallel(func(pb *testing.PB) { 96 | for pb.Next() { 97 | logger.WithFields(fakeFields()).Info(fakeMessage) 98 | } 99 | }) 100 | } 101 | 102 | func BenchmarkTextLogrus(b *testing.B) { 103 | logger := newTextLogrus(ioutil.Discard) 104 | b.ResetTimer() 105 | b.RunParallel(func(pb *testing.PB) { 106 | for pb.Next() { 107 | logger.WithFields(fakeLogrusFields()).Info(fakeMessage) 108 | } 109 | }) 110 | } 111 | 112 | func BenchmarkTextLog(b *testing.B) { 113 | logger := newTextLogger(ioutil.Discard) 114 | b.ResetTimer() 115 | b.RunParallel(func(pb *testing.PB) { 116 | for pb.Next() { 117 | logger.WithFields(fakeFields()).Info(fakeMessage) 118 | } 119 | }) 120 | } 121 | 122 | func TestCompareLogrus(t *testing.T) { 123 | key := make([]byte, 32) 124 | rand.Read(key) 125 | 126 | fmt.Println("--Logrus Text--") 127 | textLogrus := newTextLogrus(os.Stderr) 128 | textLogrus.WithFields(fakeLogrusFields()).Info("This is an informational message") 129 | textLogrus.WithFields(fakeLogrusFields()).Error("This is an error message") 130 | textLogrus.WithFields(fakeLogrusFields()).Warn("This is a warning message") 131 | textLogrus.WithFields(logrus.Fields{"round": 12345, "publicKey": key}).Debug("This is a debug message with key") 132 | textLogrus.WithFields(fakeLogrusFields()).Infof("This is a very long and inspirational message with an error: %s", errors.New("something failed")) 133 | textLogrus.WithFields(logrus.Fields{"short": true}).Info("Shortmsg") 134 | 135 | fmt.Println("\n--Log Text--") 136 | textLogger := newTextLogger(os.Stderr) 137 | textLogger.WithFields(fakeFields()).Info("This is an informational message") 138 | textLogger.WithFields(fakeFields()).Error("This is an error message") 139 | textLogger.WithFields(fakeFields()).Warn("This is a warning message") 140 | textLogger.WithFields(log.Fields{"round": 12345, "publicKey": key}).Debug("This is a debug message with key") 141 | textLogger.WithFields(fakeFields()).Infof("This is a very long and inspirational message with an error: %s", errors.New("something failed")) 142 | textLogger.WithFields(log.Fields{"short": true}).Info("Shortmsg") 143 | 144 | fmt.Println("\n--Logrus JSON--") 145 | jsonLogrus := newJSONLogrus(os.Stderr) 146 | jsonLogrus.WithFields(fakeLogrusFields()).Info(fakeMessage) 147 | jsonLogrus.WithFields(fakeLogrusFields()).Error(fakeMessage) 148 | 149 | fmt.Println("\n--Log JSON--") 150 | jsonLogger := newJSONLogger(os.Stderr) 151 | jsonLogger.WithFields(fakeFields()).Info(fakeMessage) 152 | jsonLogger.WithFields(fakeFields()).Error(fakeMessage) 153 | } 154 | -------------------------------------------------------------------------------- /bloom/bloom_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package bloom 6 | 7 | import ( 8 | "bytes" 9 | "compress/flate" 10 | "encoding/binary" 11 | "encoding/json" 12 | "log" 13 | "math" 14 | "testing" 15 | ) 16 | 17 | func TestBitset(t *testing.T) { 18 | f := New(1024, 4) 19 | for i := uint32(0); i < 1024; i++ { 20 | if f.test(i) { 21 | t.Fatalf("bit %d should not be set: %#v", i, f.data) 22 | } 23 | f.set(i) 24 | if !f.test(i) { 25 | t.Fatalf("bit %d should be set", i) 26 | } 27 | } 28 | } 29 | 30 | func TestFilter(t *testing.T) { 31 | f := New(1024, 4) 32 | if f.Test([]byte("foo")) { 33 | t.Fatalf("foo not expected") 34 | } 35 | f.Set([]byte("foo")) 36 | if !f.Test([]byte("foo")) { 37 | t.Fatalf("foo expected") 38 | } 39 | } 40 | 41 | func TestOptimal(t *testing.T) { 42 | numElementsCases := []int{100, 10000, 100000} 43 | fpRateCases := []float64{0.001, 0.00001, 0.0000001} 44 | // increasing numFP can reduce error, but makes the tests take longer 45 | numFP := []int{100, 25, 5} 46 | 47 | if testing.Short() { 48 | numElementsCases = []int{100, 100000} 49 | fpRateCases = []float64{0.001, 0.00001} 50 | numFP = []int{100, 25} 51 | } 52 | 53 | for _, numElements := range numElementsCases { 54 | for i, fpRate := range fpRateCases { 55 | f := New(Optimal(numElements, fpRate)) 56 | actualRate := f.estimateFalsePositiveRate(uint32(numElements), numFP[i]) 57 | if actualRate < fpRate { 58 | if testing.Verbose() { 59 | log.Printf("\tok: numElements=%v want %v, got %v", numElements, fpRate, actualRate) 60 | } 61 | continue 62 | } 63 | ok, err := closeEnough(fpRate, actualRate, 0.20) 64 | if ok { 65 | if testing.Verbose() { 66 | log.Printf("\tok: numElements=%v want %v, got %v (%.2f%% error)", numElements, fpRate, actualRate, err*100) 67 | } 68 | continue 69 | } 70 | 71 | t.Fatalf("numElements=%v want %v, got %v (%.2f%% error)", numElements, fpRate, actualRate, err*100) 72 | } 73 | } 74 | } 75 | 76 | func closeEnough(a, b, maxerr float64) (bool, float64) { 77 | var relerr float64 78 | if math.Abs(b) > math.Abs(a) { 79 | relerr = math.Abs((a - b) / b) 80 | } else { 81 | relerr = math.Abs((a - b) / a) 82 | } 83 | if relerr <= maxerr { 84 | return true, relerr 85 | } 86 | return false, relerr 87 | } 88 | 89 | // based on "github.com/willf/bloom" 90 | func (f *Filter) estimateFalsePositiveRate(numAdded uint32, numFP int) float64 { 91 | x := make([]byte, 4) 92 | for i := uint32(0); i < numAdded; i++ { 93 | binary.BigEndian.PutUint32(x, i) 94 | f.Set(x) 95 | } 96 | 97 | falsePositives := 0 98 | numRounds := 0 99 | for i := uint32(0); falsePositives < numFP; i++ { 100 | binary.BigEndian.PutUint32(x, numAdded+i+1) 101 | if f.Test(x) { 102 | falsePositives++ 103 | } 104 | numRounds++ 105 | } 106 | 107 | return float64(falsePositives) / float64(numRounds) 108 | } 109 | 110 | func TestOptimalSize(t *testing.T) { 111 | // These are the parameters we use in the Alpenhorn paper. 112 | numElements := 150000 113 | f := New(Optimal(numElements, 1e-10)) 114 | bs, _ := f.MarshalBinary() 115 | bitsPerElement := math.Ceil(float64(len(bs)) * 8.0 / float64(numElements)) 116 | if bitsPerElement != 48 { 117 | t.Fatalf("got %v bits per element, want %v", bitsPerElement, 48) 118 | } 119 | } 120 | 121 | func TestIncompressible(t *testing.T) { 122 | numElements := 150000 123 | filter := New(Optimal(numElements, 1e-10)) 124 | x := make([]byte, 4) 125 | for i := uint32(0); i < uint32(numElements); i++ { 126 | binary.BigEndian.PutUint32(x, i) 127 | filter.Set(x) 128 | } 129 | filterBytes, _ := filter.MarshalBinary() 130 | 131 | compressed := new(bytes.Buffer) 132 | w, _ := flate.NewWriter(compressed, 9) 133 | w.Write(filterBytes) 134 | w.Close() 135 | if compressed.Len() < len(filterBytes)*99/100 { 136 | t.Fatalf("Compressed %d -> %d", len(filterBytes), compressed.Len()) 137 | } 138 | } 139 | 140 | func TestMarshalJSON(t *testing.T) { 141 | filter := New(1000, 6) 142 | filter.Set([]byte("hello")) 143 | data, err := json.Marshal(filter) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | 148 | filter2 := new(Filter) 149 | err = json.Unmarshal(data, filter2) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | 154 | if !filter2.Test([]byte("hello")) { 155 | t.Fatal("item not in filter") 156 | } 157 | 158 | if filter.numHashes != filter2.numHashes { 159 | t.Fatalf("numHashes differ: %d != %d", filter.numHashes, filter2.numHashes) 160 | } 161 | if !bytes.Equal(filter.data, filter2.data) { 162 | t.Fatalf("filter bytes differ") 163 | } 164 | } 165 | 166 | func BenchmarkCreateLargeFilter(b *testing.B) { 167 | // dialing mu=25000; 3 servers; so each mailbox is 75000 real and 75000 noise 168 | // for a total of 150000 elements in the dialing bloom filter 169 | numElements := 150000 170 | for i := 0; i < b.N; i++ { 171 | f := New(Optimal(numElements, 1e-10)) 172 | x := make([]byte, 4) 173 | for i := uint32(0); i < uint32(numElements); i++ { 174 | binary.BigEndian.PutUint32(x, i) 175 | f.Set(x) 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /log/format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package log 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | "sort" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/davidlazar/go-crypto/encoding/base32" 20 | 21 | "vuvuzela.io/alpenhorn/log/ansi" 22 | ) 23 | 24 | var bufPool = sync.Pool{ 25 | New: func() interface{} { 26 | return new(bytes.Buffer) 27 | }, 28 | } 29 | 30 | func (e *Entry) JSON(w io.Writer) error { 31 | m := make(Fields, len(e.Fields)+3) 32 | m["time"] = e.Time 33 | m["level"] = e.Level.String() 34 | m["unixnano"] = e.Time.UnixNano() 35 | if e.Message != "" { 36 | m["msg"] = e.Message 37 | } 38 | for k, v := range e.Fields { 39 | switch v := v.(type) { 40 | case error: 41 | // Otherwise encoding/json ignores errors. 42 | m[k] = v.Error() 43 | default: 44 | m[k] = v 45 | } 46 | } 47 | return json.NewEncoder(w).Encode(m) 48 | } 49 | 50 | func OutputJSON(dst io.Writer) EntryHandler { 51 | return &outputJSON{dst} 52 | } 53 | 54 | type outputJSON struct { 55 | dst io.Writer 56 | } 57 | 58 | func (h *outputJSON) Fire(e *Entry) { 59 | err := e.JSON(h.dst) 60 | if err != nil { 61 | fmt.Fprintf(Stderr, "Error marshaling log entry to JSON: %s\n", err) 62 | return 63 | } 64 | } 65 | 66 | // OutputText is an entry handler that writes a log entry as 67 | // human-readable text to Out. The entry handler makes exactly 68 | // one call to Out.Write for each entry. 69 | type OutputText struct { 70 | Out io.Writer 71 | DisableColors bool 72 | } 73 | 74 | func (h *OutputText) Fire(e *Entry) { 75 | buf := bufPool.Get().(*bytes.Buffer) 76 | buf.Reset() 77 | 78 | h.prettyPrint(buf, e) 79 | 80 | _, err := h.Out.Write(buf.Bytes()) 81 | if err != nil { 82 | fmt.Fprintf(Stderr, "Error writing log entry: %s", err) 83 | } 84 | 85 | bufPool.Put(buf) 86 | } 87 | 88 | func (h *OutputText) prettyPrint(buf *bytes.Buffer, e *Entry) { 89 | color := e.Level.Color() 90 | timeFmt := "2006-01-02 15:04:05.000" 91 | if e.Level == InfoLevel || h.DisableColors { 92 | // Colorful timestamps on info messages is too distracting. 93 | buf.WriteString(e.Time.Format(timeFmt)) 94 | } else { 95 | ansi.WriteString(buf, e.Time.Format(timeFmt), color, ansi.Bold) 96 | } 97 | fmt.Fprintf(buf, " %s %-44s ", e.Level.Icon(), e.Message) 98 | if h.DisableColors { 99 | Logfmt(buf, e.Fields) 100 | } else { 101 | Logfmt(buf, e.Fields, color) 102 | } 103 | buf.WriteByte('\n') 104 | } 105 | 106 | type OutputDir struct { 107 | // Dir is the directory that log files are written to in JSON-line format. 108 | Dir string 109 | 110 | mu sync.Mutex 111 | currPath string 112 | currDate time.Time 113 | currFile io.WriteCloser 114 | } 115 | 116 | func (h *OutputDir) persistEntry(e *Entry) error { 117 | buf := bufPool.Get().(*bytes.Buffer) 118 | buf.Reset() 119 | defer bufPool.Put(buf) 120 | 121 | y, m, d := e.Time.Date() 122 | err := e.JSON(buf) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | h.mu.Lock() 128 | defer h.mu.Unlock() 129 | 130 | if h.currPath != "" { 131 | yy, mm, dd := h.currDate.Date() 132 | if (d == dd && m == mm && y == yy) || e.Time.Before(h.currDate) { 133 | _, err := buf.WriteTo(h.currFile) 134 | if err != nil { 135 | return fmt.Errorf("error writing log file %s: %s", h.currPath, err) 136 | } 137 | return nil 138 | } 139 | 140 | err = h.currFile.Close() 141 | if err != nil { 142 | return fmt.Errorf("error closing log file %s: %s", h.currPath, err) 143 | } 144 | } 145 | 146 | path := filepath.Join(h.Dir, e.Time.Format("2006-01-02")+".log") 147 | file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 148 | if err != nil { 149 | return fmt.Errorf("error opening log file %s: %s", path, err) 150 | } 151 | 152 | h.currDate = e.Time 153 | h.currPath = path 154 | h.currFile = file 155 | 156 | _, err = buf.WriteTo(h.currFile) 157 | if err != nil { 158 | return fmt.Errorf("error writing log file %s: %s", h.currPath, err) 159 | } 160 | return nil 161 | } 162 | 163 | func (h *OutputDir) Fire(e *Entry) { 164 | err := h.persistEntry(e) 165 | if err != nil { 166 | fmt.Fprintf(Stderr, "%s\n", err) 167 | } 168 | } 169 | 170 | func Logfmt(dst *bytes.Buffer, data map[string]interface{}, keyColors ...ansi.Code) { 171 | keys := make([]string, len(data)) 172 | i := 0 173 | for k := range data { 174 | keys[i] = k 175 | i++ 176 | } 177 | sort.Strings(keys) 178 | 179 | for _, k := range keys { 180 | dst.WriteByte(' ') 181 | ansi.WriteString(dst, k, keyColors...) 182 | dst.WriteByte('=') 183 | 184 | v := data[k] 185 | var str string 186 | switch v := v.(type) { 187 | case string: 188 | str = v 189 | case []byte: 190 | str = base32.EncodeToString(v) 191 | default: 192 | str = fmt.Sprint(v) 193 | } 194 | 195 | if needsQuotes(str) { 196 | dst.WriteString(fmt.Sprintf("%q", str)) 197 | } else { 198 | dst.WriteString(str) 199 | } 200 | } 201 | } 202 | 203 | func needsQuotes(str string) bool { 204 | if str == "" { 205 | return true 206 | } 207 | return strings.ContainsAny(str, " \\\"\t\r\n") 208 | } 209 | -------------------------------------------------------------------------------- /config/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type Server struct { 16 | persistPath string 17 | 18 | mu sync.Mutex 19 | allConfigs map[string]*SignedConfig 20 | 21 | // currentConfig is a map from service name to current config hash. 22 | currentConfig map[string]string 23 | } 24 | 25 | func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 26 | if strings.HasPrefix(r.URL.Path, "/getchain") { 27 | srv.getChainHandler(w, r) 28 | } else if strings.HasPrefix(r.URL.Path, "/current") { 29 | srv.getCurrentHandler(w, r) 30 | } else if strings.HasPrefix(r.URL.Path, "/new") { 31 | srv.newConfigHandler(w, r) 32 | } else if r.URL.Path == "/" { 33 | w.Write([]byte("Alpenhorn config server.")) 34 | } else { 35 | http.Error(w, "not found", http.StatusNotFound) 36 | } 37 | } 38 | 39 | func (srv *Server) newConfigHandler(w http.ResponseWriter, req *http.Request) { 40 | nextConfig := new(SignedConfig) 41 | if err := json.NewDecoder(req.Body).Decode(nextConfig); err != nil { 42 | http.Error(w, err.Error(), http.StatusBadRequest) 43 | return 44 | } 45 | 46 | if err := nextConfig.Validate(); err != nil { 47 | http.Error(w, fmt.Sprintf("invalid config: %s", err), http.StatusBadRequest) 48 | return 49 | } 50 | 51 | service := nextConfig.Service 52 | 53 | srv.mu.Lock() 54 | defer srv.mu.Unlock() 55 | 56 | prevHash, ok := srv.currentConfig[service] 57 | if !ok { 58 | http.Error(w, 59 | fmt.Sprintf("unknown service type: %q", service), 60 | http.StatusBadRequest, 61 | ) 62 | return 63 | } 64 | 65 | if nextConfig.PrevConfigHash != prevHash { 66 | http.Error(w, 67 | fmt.Sprintf("prev config hash does not match current config hash: got %q want %q", nextConfig.PrevConfigHash, prevHash), 68 | http.StatusBadRequest, 69 | ) 70 | return 71 | } 72 | 73 | prevConfig := srv.allConfigs[prevHash] 74 | 75 | if !nextConfig.Created.After(prevConfig.Created) { 76 | http.Error(w, 77 | fmt.Sprintf("new config was not created after previous config: %s <= %s", nextConfig.Created, prevConfig.Created), 78 | http.StatusBadRequest, 79 | ) 80 | return 81 | } 82 | 83 | if err := VerifyConfigChain(nextConfig, prevConfig); err != nil { 84 | http.Error(w, err.Error(), http.StatusBadRequest) 85 | return 86 | } 87 | 88 | nextHash := nextConfig.Hash() 89 | srv.currentConfig[service] = nextHash 90 | srv.allConfigs[nextHash] = nextConfig 91 | 92 | if err := srv.persistLocked(); err != nil { 93 | http.Error(w, fmt.Sprintf("error persisting state: %s", err), http.StatusInternalServerError) 94 | return 95 | } 96 | 97 | w.Write([]byte("updated config")) 98 | } 99 | 100 | func (srv *Server) SetCurrentConfig(config *SignedConfig) error { 101 | if err := config.Validate(); err != nil { 102 | return err 103 | } 104 | 105 | srv.mu.Lock() 106 | defer srv.mu.Unlock() 107 | 108 | hash := config.Hash() 109 | srv.allConfigs[hash] = config 110 | srv.currentConfig[config.Service] = hash 111 | 112 | return srv.persistLocked() 113 | } 114 | 115 | // CurrentConfig returns the server's current config and its hash. 116 | // The result must not be modified. 117 | func (srv *Server) CurrentConfig(service string) (*SignedConfig, string) { 118 | srv.mu.Lock() 119 | hash := srv.currentConfig[service] 120 | config := srv.allConfigs[hash] 121 | srv.mu.Unlock() 122 | return config, hash 123 | } 124 | 125 | func (srv *Server) getCurrentHandler(w http.ResponseWriter, req *http.Request) { 126 | service := req.URL.Query().Get("service") 127 | if service == "" { 128 | http.Error(w, "no service specified in query", http.StatusBadRequest) 129 | return 130 | } 131 | 132 | srv.mu.Lock() 133 | hash, ok := srv.currentConfig[service] 134 | conf := srv.allConfigs[hash] 135 | srv.mu.Unlock() 136 | 137 | if !ok { 138 | http.Error(w, fmt.Sprintf("service not found: %q", service), http.StatusBadRequest) 139 | return 140 | } 141 | 142 | json.NewEncoder(w).Encode(conf) 143 | } 144 | 145 | func (srv *Server) getChainHandler(w http.ResponseWriter, req *http.Request) { 146 | have := req.URL.Query().Get("have") 147 | if have == "" { 148 | http.Error(w, "no have hash specified in query", http.StatusBadRequest) 149 | return 150 | } 151 | want := req.URL.Query().Get("want") 152 | if want == "" { 153 | http.Error(w, "no want hash specified in query", http.StatusBadRequest) 154 | return 155 | } 156 | 157 | srv.mu.Lock() 158 | config, ok := srv.allConfigs[want] 159 | srv.mu.Unlock() 160 | if !ok { 161 | http.Error(w, "want hash not found", http.StatusBadRequest) 162 | return 163 | } 164 | 165 | configs := make([]*SignedConfig, 1) 166 | configs[0] = config 167 | 168 | prevHash := config.PrevConfigHash 169 | for prevHash != have && prevHash != "" { 170 | srv.mu.Lock() 171 | prevConfig, ok := srv.allConfigs[prevHash] 172 | srv.mu.Unlock() 173 | if !ok { 174 | panic(fmt.Sprintf("prev config not found: hash %q", prevHash)) 175 | } 176 | configs = append(configs, prevConfig) 177 | prevHash = prevConfig.PrevConfigHash 178 | } 179 | 180 | data, err := json.MarshalIndent(configs, "", " ") 181 | if err != nil { 182 | panic("json marshal error") 183 | } 184 | 185 | w.Write(data) 186 | } 187 | -------------------------------------------------------------------------------- /friendrequest.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "errors" 10 | 11 | "vuvuzela.io/alpenhorn/pkg" 12 | ) 13 | 14 | // SendFriendRequest sends a friend request to the given username using 15 | // Alpenhorn's add-friend protocol. The key is optional and specifies the 16 | // username's long-term public key if it is known ahead of time. 17 | // 18 | // The friend request is not sent right away but queued for an upcoming 19 | // add-friend round. The resulting OutgoingFriendRequest is the queued 20 | // friend request. 21 | func (c *Client) SendFriendRequest(username string, key ed25519.PublicKey) (*OutgoingFriendRequest, error) { 22 | req := &OutgoingFriendRequest{ 23 | Username: username, 24 | ExpectedKey: key, 25 | client: c, 26 | } 27 | c.mu.Lock() 28 | c.outgoingFriendRequests = append(c.outgoingFriendRequests, req) 29 | err := c.persistLocked() 30 | c.mu.Unlock() 31 | return req, err 32 | } 33 | 34 | //easyjson:readable 35 | type OutgoingFriendRequest struct { 36 | Username string 37 | ExpectedKey ed25519.PublicKey 38 | 39 | // Confirmation indicates whether this request is in response to an 40 | // incoming friend request. 41 | Confirmation bool 42 | 43 | // DialRound is the round that the resulting shared key between friends 44 | // corresponds to. This field is only used when Confirmation is true. 45 | // Otherwise, the client uses the latest dialing round when the friend 46 | // request is sent. 47 | DialRound uint32 48 | 49 | client *Client 50 | } 51 | 52 | // sentFriendRequest is the result of sending an OutgoingFriendRequest. 53 | //easyjson:readable 54 | type sentFriendRequest struct { 55 | Username string 56 | ExpectedKey ed25519.PublicKey 57 | Confirmation bool 58 | DialRound uint32 59 | 60 | SentRound uint32 61 | DHPublicKey *[32]byte 62 | DHPrivateKey *[32]byte 63 | 64 | client *Client 65 | } 66 | 67 | var ErrTooLate = errors.New("too late") 68 | 69 | // Cancel cancels the friend request by removing it from the queue. 70 | // It returns ErrTooLate if the request is not found in the queue. 71 | func (r *OutgoingFriendRequest) Cancel() error { 72 | r.client.mu.Lock() 73 | defer r.client.mu.Unlock() 74 | 75 | reqs := r.client.outgoingFriendRequests 76 | index := -1 77 | for i, c := range reqs { 78 | if r == c { 79 | index = i 80 | } 81 | } 82 | if index == -1 { 83 | return ErrTooLate 84 | } 85 | 86 | r.client.outgoingFriendRequests = append(reqs[:index], reqs[index+1:]...) 87 | err := r.client.persistLocked() 88 | return err 89 | } 90 | 91 | func (c *Client) GetOutgoingFriendRequests() []*OutgoingFriendRequest { 92 | c.mu.Lock() 93 | defer c.mu.Unlock() 94 | 95 | r := make([]*OutgoingFriendRequest, len(c.outgoingFriendRequests)) 96 | copy(r, c.outgoingFriendRequests) 97 | return r 98 | } 99 | 100 | func (c *Client) GetSentFriendRequests() []*OutgoingFriendRequest { 101 | c.mu.Lock() 102 | defer c.mu.Unlock() 103 | 104 | reqs := make([]*OutgoingFriendRequest, len(c.sentFriendRequests)) 105 | for i, req := range c.sentFriendRequests { 106 | reqs[i] = &OutgoingFriendRequest{ 107 | Username: req.Username, 108 | ExpectedKey: req.ExpectedKey, 109 | Confirmation: req.Confirmation, 110 | DialRound: req.DialRound, 111 | 112 | client: c, 113 | } 114 | } 115 | return reqs 116 | } 117 | 118 | //easyjson:readable 119 | type IncomingFriendRequest struct { 120 | Username string 121 | LongTermKey ed25519.PublicKey 122 | DHPublicKey *[32]byte 123 | DialRound uint32 124 | Verifiers []pkg.PublicServerConfig 125 | 126 | client *Client 127 | } 128 | 129 | // Approve accepts the friend request and queues a confirmation friend 130 | // request. The add-friend protocol is complete for this friend when the 131 | // confirmation request is sent. Approve assumes that the friend request 132 | // has not been previously rejected. 133 | func (r *IncomingFriendRequest) Approve() (*OutgoingFriendRequest, error) { 134 | out := &OutgoingFriendRequest{ 135 | Username: r.Username, 136 | Confirmation: true, 137 | DialRound: r.DialRound, 138 | } 139 | c := r.client 140 | c.mu.Lock() 141 | c.outgoingFriendRequests = append(c.outgoingFriendRequests, out) 142 | // The incoming request stays in its queue so it can be matched to the 143 | // outgoing request when it is sent. 144 | err := c.persistLocked() 145 | c.mu.Unlock() 146 | return out, err 147 | } 148 | 149 | // Reject rejects the friend request, returning ErrTooLate if the 150 | // friend request is not found in the client's queue. 151 | func (r *IncomingFriendRequest) Reject() error { 152 | r.client.mu.Lock() 153 | defer r.client.mu.Unlock() 154 | 155 | reqs := r.client.incomingFriendRequests 156 | index := -1 157 | for i, c := range reqs { 158 | if r == c { 159 | index = i 160 | } 161 | } 162 | if index == -1 { 163 | return ErrTooLate 164 | } 165 | 166 | r.client.incomingFriendRequests = append(reqs[:index], reqs[index+1:]...) 167 | err := r.client.persistLocked() 168 | return err 169 | } 170 | 171 | func (c *Client) GetIncomingFriendRequests() []*IncomingFriendRequest { 172 | c.mu.Lock() 173 | defer c.mu.Unlock() 174 | 175 | r := make([]*IncomingFriendRequest, len(c.incomingFriendRequests)) 176 | copy(r, c.incomingFriendRequests) 177 | return r 178 | } 179 | -------------------------------------------------------------------------------- /encoding/toml/decode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package toml 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | type config struct { 15 | Entry string 16 | PublicKey ed25519.PublicKey 17 | PrivateKey ed25519.PrivateKey 18 | ExtraData []byte 19 | Count int 20 | Servers map[string]serverInfo 21 | Clients []clientInfo `mapstructure:"client"` 22 | } 23 | 24 | type serverInfo struct { 25 | IP string 26 | Mu int 27 | B float64 28 | Wait time.Duration 29 | Optional []byte 30 | } 31 | 32 | type clientInfo struct { 33 | Username string 34 | Friends map[string]ed25519.PublicKey 35 | } 36 | 37 | const tomlConfig = ` 38 | entry = "192.168.0.1" 39 | publicKey = "gg3rwp4ye8j1xbmkf2y5ae55cne1y3m9ew8g3156g8n5c572j2d0" 40 | privateKey = "dmrrz794yevgkb0gk0qqagzsym4d294eckbj2dq1khcpksnj654881web2f7490ynt9qhf2n72jpaq0z1t4qe481gjk84ajp2kh916g" 41 | extraData = "928vmmzbwh746grq3n1xp497m9m2jn4t2948njqf4bd841ykv6xg" 42 | count = 42 43 | 44 | [servers] 45 | 46 | [servers.alpha] 47 | ip = "10.0.0.1" 48 | mu = 3000 49 | b = 72.5 50 | wait = "30s" 51 | 52 | [servers.beta] 53 | ip = "10.0.0.2" 54 | mu = 9000 55 | b = 4000.714 56 | wait = 1000000000 57 | optional = [3, 0, 1, 2] 58 | 59 | [[client]] 60 | username = "alice" 61 | 62 | [client.friends] 63 | bob = "m3vzyq6r1m27m1se385qhdprzbab6xhyy6ftv5w3mhttej3qmdp0" 64 | eve = "2myv6p59nb9a7g2n27etd4cv3mhcznp4hc2z0dm18cksasajs10g" 65 | 66 | [[client]] 67 | username = "sam" 68 | 69 | [client.friends] 70 | eve = "d3311ab5xyzmw6r5tmffama53xct661ky0yb9hrm1xfmvzh9gnk0" 71 | ` 72 | 73 | func TestDecode(t *testing.T) { 74 | c := new(config) 75 | err := Unmarshal([]byte(tomlConfig), c) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | pub := ed25519.PublicKey{0x84, 0x7, 0x8e, 0x58, 0x9e, 0x72, 0x24, 0x1e, 0xae, 0x93, 0x78, 0xbc, 0x55, 0x38, 0xa5, 0x65, 0x5c, 0x1f, 0xe, 0x89, 0x77, 0x11, 0x1, 0x84, 0xa6, 0x82, 0x2a, 0x56, 0x14, 0xe2, 0x90, 0x9a} 81 | priv := ed25519.PrivateKey{0x6d, 0x31, 0x8f, 0x9d, 0x24, 0xf3, 0xb7, 0x9, 0xac, 0x10, 0x98, 0x2f, 0x75, 0x43, 0xf9, 0xf5, 0x8, 0xd1, 0x24, 0x8e, 0x64, 0xd7, 0x21, 0x36, 0xe1, 0x9c, 0x59, 0x69, 0xe6, 0xb2, 0x31, 0x48, 0x84, 0x7, 0x8e, 0x58, 0x9e, 0x72, 0x24, 0x1e, 0xae, 0x93, 0x78, 0xbc, 0x55, 0x38, 0xa5, 0x65, 0x5c, 0x1f, 0xe, 0x89, 0x77, 0x11, 0x1, 0x84, 0xa6, 0x82, 0x2a, 0x56, 0x14, 0xe2, 0x90, 0x9a} 82 | extraData := []byte{0x48, 0x91, 0xba, 0x53, 0xeb, 0xe4, 0x4e, 0x43, 0x43, 0x17, 0x1d, 0x43, 0xdb, 0x11, 0x27, 0xa2, 0x68, 0x29, 0x54, 0x9a, 0x12, 0x48, 0x8a, 0xca, 0xef, 0x22, 0xda, 0x82, 0x7, 0xd3, 0xd9, 0xbb} 83 | 84 | if !bytes.Equal(c.ExtraData, extraData) { 85 | t.Fatalf("unexpected extra data in config: %#v", c.ExtraData) 86 | } 87 | if !bytes.Equal(c.PrivateKey, priv) { 88 | t.Fatalf("unexpected private key in config: %#v", c.PrivateKey) 89 | } 90 | if !bytes.Equal(c.PublicKey, pub) { 91 | t.Fatalf("unexpected public key in config: %#v", c.PublicKey) 92 | } 93 | 94 | if c.Servers["alpha"].IP != "10.0.0.1" { 95 | t.Fatalf("unexpected IP for server alpha: %s", c.Servers["alpha"].IP) 96 | } 97 | if c.Servers["beta"].IP != "10.0.0.2" { 98 | t.Fatalf("unexpected IP for server beta: %s", c.Servers["beta"].IP) 99 | } 100 | 101 | if c.Servers["alpha"].B != float64(72.5) { 102 | t.Fatalf("unexpected b value for server alpha: %v", c.Servers["alpha"].B) 103 | } 104 | if c.Servers["beta"].B != float64(4000.714) { 105 | t.Fatalf("unexpected b value for server beta: %v", c.Servers["beta"].B) 106 | } 107 | 108 | if c.Servers["alpha"].Wait != time.Duration(30*time.Second) { 109 | t.Fatalf("unexpected wait value for server alpha: %#v", c.Servers["alpha"].Wait) 110 | } 111 | if c.Servers["beta"].Wait != time.Duration(1*time.Second) { 112 | t.Fatalf("unexpected wait value for server beta: %#v", c.Servers["beta"].Wait) 113 | } 114 | 115 | if !bytes.Equal(c.Servers["beta"].Optional, []byte{3, 0, 1, 2}) { 116 | t.Fatalf("unexpected optional value for server beta: %#v", c.Servers["beta"].Optional) 117 | } 118 | 119 | if len(c.Clients) != 2 { 120 | t.Fatalf("unexpected number of clients, got %d want %d", len(c.Clients), 2) 121 | } 122 | if c.Clients[0].Username != "alice" { 123 | t.Fatalf("wrong username for first client, got %q want %q", c.Clients[0].Username, "alice") 124 | } 125 | if len(c.Clients[0].Friends) != 2 { 126 | t.Fatalf("unexpected number of friends for first client, got %d want %d", len(c.Clients[0].Friends), 2) 127 | } 128 | if !bytes.Equal(c.Clients[0].Friends["bob"], decodeBytes("m3vzyq6r1m27m1se385qhdprzbab6xhyy6ftv5w3mhttej3qmdp0")) { 129 | t.Fatalf("bad key for bob in client 1, got %s", EncodeBytes(c.Clients[0].Friends["bob"])) 130 | } 131 | if !bytes.Equal(c.Clients[0].Friends["eve"], decodeBytes("2myv6p59nb9a7g2n27etd4cv3mhcznp4hc2z0dm18cksasajs10g")) { 132 | t.Fatalf("bad key for eve in client 1, got %s", EncodeBytes(c.Clients[0].Friends["eve"])) 133 | } 134 | if c.Clients[1].Username != "sam" { 135 | t.Fatalf("wrong username for first client, got %q want %q", c.Clients[1].Username, "sam") 136 | } 137 | if !bytes.Equal(c.Clients[1].Friends["eve"], decodeBytes("d3311ab5xyzmw6r5tmffama53xct661ky0yb9hrm1xfmvzh9gnk0")) { 138 | t.Fatalf("bad key for eve in client 2, got %s", EncodeBytes(c.Clients[1].Friends["eve"])) 139 | } 140 | } 141 | 142 | func decodeBytes(str string) []byte { 143 | data, err := DecodeBytes(str) 144 | if err != nil { 145 | panic(err) 146 | } 147 | return data 148 | } 149 | -------------------------------------------------------------------------------- /persist.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "encoding/json" 10 | "io/ioutil" 11 | 12 | "vuvuzela.io/alpenhorn/config" 13 | "vuvuzela.io/internal/ioutil2" 14 | ) 15 | 16 | //easyjson:readable 17 | type persistedState struct { 18 | Username string 19 | LongTermPublicKey ed25519.PublicKey 20 | LongTermPrivateKey ed25519.PrivateKey 21 | PKGLoginKey ed25519.PrivateKey 22 | 23 | AddFriendConfig *config.SignedConfig 24 | DialingConfig *config.SignedConfig 25 | 26 | IncomingFriendRequests []*IncomingFriendRequest 27 | OutgoingFriendRequests []*OutgoingFriendRequest 28 | SentFriendRequests []*sentFriendRequest 29 | Friends map[string]*persistedFriend 30 | } 31 | 32 | // persistedFriend is the persisted representation of the Friend type. 33 | // We use this because Friend.extraData is unexported but must be persisted. 34 | //easyjson:readable 35 | type persistedFriend struct { 36 | Username string 37 | LongTermKey ed25519.PublicKey 38 | ExtraData []byte 39 | } 40 | 41 | // LoadClient loads a client from persisted state at the given path. 42 | // You should set the client's KeywheelPersistPath before connecting. 43 | func LoadClient(clientPersistPath, keywheelPersistPath string) (*Client, error) { 44 | clientData, err := ioutil.ReadFile(clientPersistPath) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | st := new(persistedState) 50 | err = json.Unmarshal(clientData, st) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | keywheelData, err := ioutil.ReadFile(keywheelPersistPath) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | c := &Client{ 61 | ClientPersistPath: clientPersistPath, 62 | KeywheelPersistPath: keywheelPersistPath, 63 | } 64 | err = c.wheel.UnmarshalBinary(keywheelData) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | c.loadStateLocked(st) 70 | return c, nil 71 | } 72 | 73 | func (c *Client) loadStateLocked(st *persistedState) { 74 | c.Username = st.Username 75 | c.LongTermPublicKey = st.LongTermPublicKey 76 | c.LongTermPrivateKey = st.LongTermPrivateKey 77 | c.PKGLoginKey = st.PKGLoginKey 78 | 79 | c.addFriendConfig = st.AddFriendConfig 80 | c.addFriendConfigHash = st.AddFriendConfig.Hash() 81 | 82 | c.dialingConfig = st.DialingConfig 83 | c.dialingConfigHash = st.DialingConfig.Hash() 84 | 85 | c.incomingFriendRequests = st.IncomingFriendRequests 86 | c.outgoingFriendRequests = st.OutgoingFriendRequests 87 | c.sentFriendRequests = st.SentFriendRequests 88 | 89 | for _, req := range c.incomingFriendRequests { 90 | req.client = c 91 | } 92 | for _, req := range c.outgoingFriendRequests { 93 | req.client = c 94 | } 95 | for _, req := range c.sentFriendRequests { 96 | req.client = c 97 | } 98 | 99 | c.friends = make(map[string]*Friend, len(st.Friends)) 100 | for username, friend := range st.Friends { 101 | c.friends[username] = &Friend{ 102 | Username: friend.Username, 103 | LongTermKey: friend.LongTermKey, 104 | extraData: friend.ExtraData, 105 | client: c, 106 | } 107 | } 108 | } 109 | 110 | // Persist writes the client's state to disk. The client persists 111 | // itself automatically, so Persist is only needed when creating 112 | // a new client. 113 | func (c *Client) Persist() error { 114 | c.mu.Lock() 115 | err := c.persistLocked() 116 | c.mu.Unlock() 117 | return err 118 | } 119 | 120 | // persistLocked persists the client state and keywheel state, assuming 121 | // c.mu is locked. The keywheel and client state are usually persisted 122 | // at the same time to avoid leaking metadata. 123 | func (c *Client) persistLocked() error { 124 | err := c.persistClientLocked() 125 | if e := c.persistKeywheelLocked(); err == nil { 126 | err = e 127 | } 128 | return err 129 | } 130 | 131 | func (c *Client) persistClient() error { 132 | c.mu.Lock() 133 | err := c.persistClientLocked() 134 | c.mu.Unlock() 135 | return err 136 | } 137 | 138 | func (c *Client) persistClientLocked() error { 139 | if c.ClientPersistPath == "" { 140 | return nil 141 | } 142 | 143 | st := &persistedState{ 144 | Username: c.Username, 145 | LongTermPublicKey: c.LongTermPublicKey, 146 | LongTermPrivateKey: c.LongTermPrivateKey, 147 | PKGLoginKey: c.PKGLoginKey, 148 | 149 | AddFriendConfig: c.addFriendConfig, 150 | DialingConfig: c.dialingConfig, 151 | 152 | IncomingFriendRequests: c.incomingFriendRequests, 153 | OutgoingFriendRequests: c.outgoingFriendRequests, 154 | SentFriendRequests: c.sentFriendRequests, 155 | 156 | Friends: make(map[string]*persistedFriend, len(c.friends)), 157 | } 158 | 159 | for username, friend := range c.friends { 160 | st.Friends[username] = &persistedFriend{ 161 | Username: friend.Username, 162 | LongTermKey: friend.LongTermKey, 163 | ExtraData: friend.extraData, 164 | } 165 | } 166 | 167 | data, err := json.MarshalIndent(st, "", " ") 168 | if err != nil { 169 | return err 170 | } 171 | 172 | return ioutil2.WriteFileAtomic(c.ClientPersistPath, data, 0600) 173 | } 174 | 175 | func (c *Client) persistKeywheel() error { 176 | c.mu.Lock() 177 | err := c.persistKeywheelLocked() 178 | c.mu.Unlock() 179 | return err 180 | } 181 | 182 | func (c *Client) persistKeywheelLocked() error { 183 | if c.KeywheelPersistPath == "" { 184 | return nil 185 | } 186 | 187 | data, err := c.wheel.MarshalBinary() 188 | if err != nil { 189 | return err 190 | } 191 | 192 | return ioutil2.WriteFileAtomic(c.KeywheelPersistPath, data, 0600) 193 | } 194 | -------------------------------------------------------------------------------- /typesocket/hub.go: -------------------------------------------------------------------------------- 1 | // Package typesocket implements a websocket server and client. 2 | package typesocket 3 | 4 | import ( 5 | "errors" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | 12 | "vuvuzela.io/alpenhorn/log" 13 | ) 14 | 15 | // The Hub and serverConn methods are based on 16 | // https://github.com/gorilla/websocket/blob/master/examples/chat/ 17 | 18 | const ( 19 | // Time allowed to write a message to the peer. 20 | writeWait = 10 * time.Second 21 | 22 | // Time allowed to read the next pong message from the peer. 23 | pongWait = 30 * time.Second 24 | 25 | // Send pings to peer with this period. Must be less than pongWait. 26 | pingPeriod = 20 * time.Second 27 | 28 | // Maximum message size allowed from peer. 29 | maxMessageSize = 16384 30 | ) 31 | 32 | type Hub struct { 33 | Mux Mux 34 | 35 | // OnConnect is called when a client connects to the server. 36 | OnConnect func(Conn) error 37 | 38 | mu sync.Mutex 39 | conns map[*serverConn]bool 40 | } 41 | 42 | type serverConn struct { 43 | hub *Hub 44 | conn *websocket.Conn 45 | send chan []byte 46 | 47 | mu sync.Mutex 48 | closed bool 49 | } 50 | 51 | // readPump pumps messages from the websocket connection to the hub. 52 | func (c *serverConn) readPump() { 53 | defer func() { 54 | c.mu.Lock() 55 | if !c.closed { 56 | c.closed = true 57 | close(c.send) 58 | } 59 | c.mu.Unlock() 60 | c.hub.unregister(c) 61 | c.conn.Close() 62 | }() 63 | c.conn.SetReadLimit(maxMessageSize) 64 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 65 | c.conn.SetPongHandler(func(string) error { 66 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 67 | return nil 68 | }) 69 | for { 70 | var e envelope 71 | err := c.conn.ReadJSON(&e) 72 | if err != nil { 73 | switch { 74 | case websocket.IsCloseError(err, websocket.CloseGoingAway): 75 | // all good 76 | case websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway): 77 | log.Errorf("hub: unexpected close error: %v", err) 78 | default: 79 | log.Errorf("hub: ReadJSON error: %s", err) 80 | } 81 | break 82 | } 83 | c.hub.Mux.openEnvelope(c, &e) 84 | } 85 | } 86 | 87 | // write writes a message with the given message type and payload. 88 | func (c *serverConn) write(mt int, payload []byte) error { 89 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 90 | return c.conn.WriteMessage(mt, payload) 91 | } 92 | 93 | // writePump pumps messages from the hub to the websocket connection. 94 | func (c *serverConn) writePump() { 95 | ticker := time.NewTicker(pingPeriod) 96 | defer func() { 97 | ticker.Stop() 98 | c.conn.Close() 99 | }() 100 | for { 101 | select { 102 | case message, ok := <-c.send: 103 | if !ok { 104 | // The hub closed the channel. 105 | c.write(websocket.CloseMessage, []byte{}) 106 | return 107 | } 108 | 109 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 110 | w, err := c.conn.NextWriter(websocket.TextMessage) 111 | if err != nil { 112 | log.Errorf("hub: write error: %s", err) 113 | return 114 | } 115 | w.Write(message) 116 | 117 | if err := w.Close(); err != nil { 118 | log.Errorf("hub: write (close) error: %s", err) 119 | return 120 | } 121 | case <-ticker.C: 122 | if err := c.write(websocket.PingMessage, []byte{}); err != nil { 123 | log.Errorf("hub: write (ping) error: %s", err) 124 | return 125 | } 126 | } 127 | } 128 | } 129 | 130 | func (c *serverConn) Send(msgID string, v interface{}) error { 131 | msg, err := encodeMessage(msgID, v) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | c.mu.Lock() 137 | if c.closed { 138 | c.mu.Unlock() 139 | return errors.New("connection closed") 140 | } 141 | 142 | select { 143 | case c.send <- msg: 144 | c.mu.Unlock() 145 | return nil 146 | default: 147 | c.closed = true 148 | close(c.send) 149 | c.mu.Unlock() 150 | c.hub.unregister(c) 151 | return errors.New("failed to send") 152 | } 153 | } 154 | 155 | func (c *serverConn) Close() error { 156 | return c.conn.Close() 157 | } 158 | 159 | var upgrader = websocket.Upgrader{ 160 | ReadBufferSize: 4096, 161 | WriteBufferSize: 4096, 162 | } 163 | 164 | func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) { 165 | if r.Method != "GET" { 166 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 167 | return 168 | } 169 | 170 | ws, err := upgrader.Upgrade(w, r, nil) 171 | if err != nil { 172 | log.Errorf("hub: Upgrade error: %s", err) 173 | return 174 | } 175 | 176 | c := &serverConn{ 177 | hub: h, 178 | conn: ws, 179 | send: make(chan []byte, 64), 180 | } 181 | h.register(c) 182 | 183 | if h.OnConnect != nil { 184 | err := h.OnConnect(c) 185 | if err != nil { 186 | http.Error(w, "connection error", http.StatusInternalServerError) 187 | return 188 | } 189 | } 190 | 191 | go c.writePump() 192 | c.readPump() 193 | } 194 | 195 | func (h *Hub) register(c *serverConn) { 196 | h.mu.Lock() 197 | if h.conns == nil { 198 | h.conns = make(map[*serverConn]bool) 199 | } 200 | h.conns[c] = true 201 | h.mu.Unlock() 202 | } 203 | 204 | func (h *Hub) unregister(c *serverConn) { 205 | h.mu.Lock() 206 | _, ok := h.conns[c] 207 | if ok { 208 | delete(h.conns, c) 209 | } 210 | h.mu.Unlock() 211 | } 212 | 213 | func (h *Hub) Broadcast(msgID string, v interface{}) error { 214 | msg, err := encodeMessage(msgID, v) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | h.mu.Lock() 220 | defer h.mu.Unlock() 221 | 222 | for conn := range h.conns { 223 | conn.mu.Lock() 224 | if conn.closed { 225 | conn.mu.Unlock() 226 | continue 227 | } 228 | 229 | select { 230 | case conn.send <- msg: 231 | default: 232 | delete(h.conns, conn) 233 | conn.closed = true 234 | close(conn.send) 235 | } 236 | conn.mu.Unlock() 237 | } 238 | return nil 239 | } 240 | -------------------------------------------------------------------------------- /dialing/mixer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package dialing provides functionality for Alpenhorn's dialing protocol. 6 | package dialing 7 | 8 | import ( 9 | "bytes" 10 | "crypto/ed25519" 11 | "encoding/binary" 12 | "encoding/gob" 13 | "encoding/json" 14 | "fmt" 15 | "io/ioutil" 16 | "net/http" 17 | "strconv" 18 | "sync" 19 | "unsafe" 20 | 21 | "vuvuzela.io/alpenhorn/bloom" 22 | "vuvuzela.io/alpenhorn/edhttp" 23 | "vuvuzela.io/alpenhorn/errors" 24 | "vuvuzela.io/concurrency" 25 | "vuvuzela.io/crypto/onionbox" 26 | "vuvuzela.io/crypto/rand" 27 | "vuvuzela.io/crypto/shuffle" 28 | "vuvuzela.io/vuvuzela/mixnet" 29 | ) 30 | 31 | const ( 32 | // SizeToken is the number of bytes in a dialing token. 33 | SizeToken = 32 34 | 35 | sizeMixMessage = int(unsafe.Sizeof(MixMessage{})) 36 | ) 37 | 38 | type MixMessage struct { 39 | Mailbox uint32 40 | Token [SizeToken]byte 41 | } 42 | 43 | type Mixer struct { 44 | SigningKey ed25519.PrivateKey 45 | 46 | Laplace rand.Laplace 47 | 48 | once sync.Once 49 | cdnClient *edhttp.Client 50 | } 51 | 52 | func (srv *Mixer) Bidirectional() bool { 53 | return false 54 | } 55 | 56 | func (srv *Mixer) SizeIncomingMessage() int { 57 | return sizeMixMessage 58 | } 59 | 60 | func (srv *Mixer) SizeReplyMessage() int { 61 | return -1 // only used in bidirectional mode 62 | } 63 | 64 | type ServiceData struct { 65 | CDNKey ed25519.PublicKey 66 | CDNAddress string 67 | NumMailboxes uint32 68 | } 69 | 70 | const DialingServiceDataVersion = 0 71 | 72 | func (srv *Mixer) ParseServiceData(data []byte) (interface{}, error) { 73 | d := new(ServiceData) 74 | err := d.Unmarshal(data) 75 | return d, err 76 | } 77 | 78 | func (srv *Mixer) GenerateNoise(settings mixnet.RoundSettings, myPos int) [][]byte { 79 | noiseTotal := uint32(0) 80 | noiseCounts := make([]uint32, settings.ServiceData.(*ServiceData).NumMailboxes+1) 81 | for b := range noiseCounts { 82 | bmu := srv.Laplace.Uint32() 83 | noiseCounts[b] = bmu 84 | noiseTotal += bmu 85 | } 86 | noise := make([][]byte, noiseTotal) 87 | 88 | mailbox := make([]uint32, len(noise)) 89 | idx := 0 90 | for b, count := range noiseCounts { 91 | for i := uint32(0); i < count; i++ { 92 | mailbox[idx] = uint32(b) 93 | idx++ 94 | } 95 | } 96 | 97 | nextServerKeys := settings.OnionKeys[myPos+1:] 98 | 99 | concurrency.ParallelFor(len(noise), func(p *concurrency.P) { 100 | for i, ok := p.Next(); ok; i, ok = p.Next() { 101 | var exchange [sizeMixMessage]byte 102 | binary.BigEndian.PutUint32(exchange[0:4], mailbox[i]) 103 | if mailbox[i] != 0 { 104 | rand.Read(exchange[4:]) 105 | } 106 | onion, _ := onionbox.Seal(exchange[:], mixnet.ForwardNonce(settings.Round), nextServerKeys) 107 | noise[i] = onion 108 | } 109 | }) 110 | 111 | return noise 112 | } 113 | 114 | func (srv *Mixer) HandleMessages(settings mixnet.RoundSettings, messages [][]byte) (interface{}, error) { 115 | srv.once.Do(func() { 116 | srv.cdnClient = &edhttp.Client{ 117 | Key: srv.SigningKey, 118 | } 119 | }) 120 | 121 | serviceData := settings.ServiceData.(*ServiceData) 122 | 123 | // The last server doesn't shuffle by default, so shuffle here. 124 | shuffler := shuffle.New(rand.Reader, len(messages)) 125 | shuffler.Shuffle(messages) 126 | 127 | groups := make(map[uint32][][]byte) 128 | 129 | for _, m := range messages { 130 | if len(m) != sizeMixMessage { 131 | continue 132 | } 133 | mx := new(MixMessage) 134 | if err := mx.UnmarshalBinary(m); err != nil { 135 | continue 136 | } 137 | if mx.Mailbox == 0 { 138 | continue // dummy dead drop 139 | } 140 | groups[mx.Mailbox] = append(groups[mx.Mailbox], mx.Token[:]) 141 | } 142 | 143 | mailboxes := make(map[string][]byte) 144 | for mbox, tokens := range groups { 145 | f := bloom.New(bloom.Optimal(len(tokens), 0.000001)) 146 | for _, token := range tokens { 147 | f.Set(token) 148 | } 149 | mstr := strconv.FormatUint(uint64(mbox), 10) 150 | mailboxes[mstr], _ = f.MarshalBinary() 151 | } 152 | 153 | buf := new(bytes.Buffer) 154 | err := gob.NewEncoder(buf).Encode(mailboxes) 155 | if err != nil { 156 | return "", errors.Wrap(err, "gob.Encode") 157 | } 158 | 159 | putURL := fmt.Sprintf("https://%s/put?bucket=%s/%d", serviceData.CDNAddress, settings.Service, settings.Round) 160 | resp, err := srv.cdnClient.Post(serviceData.CDNKey, putURL, "application/octet-stream", buf) 161 | if err != nil { 162 | return "", err 163 | } 164 | defer resp.Body.Close() 165 | if resp.StatusCode != http.StatusOK { 166 | msg, _ := ioutil.ReadAll(resp.Body) 167 | err = errors.New("bad CDN response: %s: %q", resp.Status, msg) 168 | return "", err 169 | } 170 | 171 | getURL := fmt.Sprintf("https://%s/get?bucket=%s/%d", serviceData.CDNAddress, settings.Service, settings.Round) 172 | return getURL, nil 173 | } 174 | 175 | func (e *MixMessage) MarshalBinary() ([]byte, error) { 176 | buf := new(bytes.Buffer) 177 | if err := binary.Write(buf, binary.BigEndian, e); err != nil { 178 | return nil, err 179 | } 180 | return buf.Bytes(), nil 181 | } 182 | 183 | func (e *MixMessage) UnmarshalBinary(data []byte) error { 184 | buf := bytes.NewReader(data) 185 | return binary.Read(buf, binary.BigEndian, e) 186 | } 187 | 188 | func (d *ServiceData) Unmarshal(data []byte) error { 189 | if len(data) == 0 { 190 | return errors.New("empty raw service data") 191 | } 192 | if data[0] != DialingServiceDataVersion { 193 | return errors.New("invalid version: %d", data[0]) 194 | } 195 | return json.Unmarshal(data[1:], d) 196 | } 197 | 198 | func (d ServiceData) Marshal() []byte { 199 | buf := new(bytes.Buffer) 200 | buf.WriteByte(DialingServiceDataVersion) 201 | err := json.NewEncoder(buf).Encode(d) 202 | if err != nil { 203 | panic(err) 204 | } 205 | return buf.Bytes() 206 | } 207 | -------------------------------------------------------------------------------- /keywheel/keywheel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package keywheel implements Alpenhorn's keywheel construction. 6 | package keywheel 7 | 8 | import ( 9 | "bytes" 10 | "crypto/hmac" 11 | "crypto/sha256" 12 | "encoding/binary" 13 | "encoding/json" 14 | "fmt" 15 | "sync" 16 | ) 17 | 18 | // Use github.com/davidlazar/easyjson: 19 | //go:generate easyjson . 20 | 21 | const version byte = 1 22 | 23 | type Wheel struct { 24 | mu sync.Mutex 25 | secrets map[string]*roundSecret 26 | } 27 | 28 | //easyjson:readable 29 | type roundSecret struct { 30 | Round uint32 31 | Secret *[32]byte 32 | } 33 | 34 | func (rs roundSecret) getSecret(round uint32) *[32]byte { 35 | if rs.Round == round { 36 | return rs.Secret 37 | } 38 | if rs.Round > round { 39 | return nil 40 | } 41 | 42 | secret := rs.Secret 43 | for r := rs.Round; r < round; r++ { 44 | secret = hash1(secret, r) 45 | } 46 | 47 | return secret 48 | } 49 | 50 | func (w *Wheel) Put(username string, round uint32, secret *[32]byte) { 51 | w.mu.Lock() 52 | if w.secrets == nil { 53 | w.secrets = make(map[string]*roundSecret) 54 | } 55 | w.secrets[username] = &roundSecret{ 56 | Round: round, 57 | Secret: secret, 58 | } 59 | w.mu.Unlock() 60 | } 61 | 62 | func (w *Wheel) get(username string) *roundSecret { 63 | w.mu.Lock() 64 | defer w.mu.Unlock() 65 | if w.secrets == nil { 66 | return nil 67 | } 68 | return w.secrets[username] 69 | } 70 | 71 | // Exists returns true if username is in the keywheel 72 | // and false otherwise. 73 | func (w *Wheel) Exists(username string) bool { 74 | return w.get(username) != nil 75 | } 76 | 77 | // UnsafeGet returns the internal keywheel state for a username. 78 | // This is unsafe; use SessionKey, if possible. 79 | func (w *Wheel) UnsafeGet(username string) (round uint32, secret *[32]byte) { 80 | rs := w.get(username) 81 | if rs != nil { 82 | round = rs.Round 83 | secret = rs.Secret 84 | } 85 | return 86 | } 87 | 88 | func (w *Wheel) Remove(username string) { 89 | w.mu.Lock() 90 | delete(w.secrets, username) 91 | w.mu.Unlock() 92 | } 93 | 94 | func (w *Wheel) SessionKey(username string, round uint32) *[32]byte { 95 | rs := w.get(username) 96 | if rs == nil || rs.Round > round { 97 | return nil 98 | } 99 | 100 | // TODO should we hash the intent also? 101 | key := hash3(rs.getSecret(round), round) 102 | return key 103 | } 104 | 105 | func (w *Wheel) OutgoingDialToken(username string, round uint32, intent int) *[32]byte { 106 | rs := w.get(username) 107 | if rs == nil || rs.Round > round { 108 | return nil 109 | } 110 | 111 | key := rs.getSecret(round) 112 | token := hash2(key, round, username, intent) 113 | return token 114 | } 115 | 116 | type UserDialTokens struct { 117 | FromUsername string 118 | Tokens []*[32]byte 119 | } 120 | 121 | func (w *Wheel) IncomingDialTokens(myUsername string, round uint32, numIntents int) []*UserDialTokens { 122 | w.mu.Lock() 123 | defer w.mu.Unlock() 124 | 125 | all := make([]*UserDialTokens, 0, len(w.secrets)) 126 | for friend, rs := range w.secrets { 127 | if rs.Round > round { 128 | continue 129 | } 130 | u := &UserDialTokens{ 131 | FromUsername: friend, 132 | Tokens: make([]*[32]byte, numIntents), 133 | } 134 | key := rs.getSecret(round) 135 | for i := range u.Tokens { 136 | u.Tokens[i] = hash2(key, round, myUsername, i) 137 | } 138 | all = append(all, u) 139 | } 140 | return all 141 | } 142 | 143 | func (w *Wheel) EraseKeys(round uint32) { 144 | w.mu.Lock() 145 | defer w.mu.Unlock() 146 | 147 | newRound := round + 1 148 | for _, rs := range w.secrets { 149 | newSecret := rs.getSecret(newRound) 150 | if newSecret != nil { 151 | rs.Round = newRound 152 | rs.Secret = newSecret 153 | } 154 | } 155 | } 156 | 157 | func (w *Wheel) MarshalBinary() ([]byte, error) { 158 | w.mu.Lock() 159 | defer w.mu.Unlock() 160 | 161 | buf := new(bytes.Buffer) 162 | if _, err := buf.Write([]byte{version}); err != nil { 163 | return nil, err 164 | } 165 | 166 | encoder := json.NewEncoder(buf) 167 | // use indented json for now for easier debugging 168 | encoder.SetIndent("", " ") 169 | err := encoder.Encode(w.secrets) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | return buf.Bytes(), nil 175 | } 176 | 177 | func (w *Wheel) UnmarshalBinary(data []byte) error { 178 | w.mu.Lock() 179 | defer w.mu.Unlock() 180 | 181 | ver := data[0] 182 | if ver != version { 183 | return fmt.Errorf("unknown serialization version: %d", ver) 184 | } 185 | 186 | secrets := make(map[string]*roundSecret) 187 | err := json.Unmarshal(data[1:], &secrets) 188 | if err != nil { 189 | return err 190 | } 191 | w.secrets = secrets 192 | 193 | return nil 194 | } 195 | 196 | var ( 197 | hash1UniqueBytes = []byte{1, 1, 1, 1} 198 | hash2UniqueBytes = []byte{2, 2, 2, 2} 199 | hash3UniqueBytes = []byte{3, 3, 3, 3} 200 | ) 201 | 202 | func hash1(key *[32]byte, round uint32) *[32]byte { 203 | var rb [4]byte 204 | binary.BigEndian.PutUint32(rb[:], round) 205 | 206 | h := hmac.New(sha256.New, key[:]) 207 | h.Write(hash1UniqueBytes) 208 | h.Write(rb[:]) 209 | 210 | r := new([32]byte) 211 | copy(r[:], h.Sum(nil)) 212 | return r 213 | } 214 | 215 | func hash2(key *[32]byte, round uint32, username string, intent int) *[32]byte { 216 | var eb [8]byte 217 | binary.BigEndian.PutUint32(eb[0:4], round) 218 | binary.BigEndian.PutUint32(eb[4:8], uint32(intent)) 219 | 220 | h := hmac.New(sha256.New, key[:]) 221 | h.Write(hash2UniqueBytes) 222 | h.Write(eb[:]) 223 | h.Write([]byte(username)) 224 | 225 | r := new([32]byte) 226 | copy(r[:], h.Sum(nil)) 227 | return r 228 | } 229 | 230 | func hash3(key *[32]byte, round uint32) *[32]byte { 231 | var rb [4]byte 232 | binary.BigEndian.PutUint32(rb[:], round) 233 | 234 | h := hmac.New(sha256.New, key[:]) 235 | h.Write(hash3UniqueBytes) 236 | h.Write(rb[:]) 237 | 238 | r := new([32]byte) 239 | copy(r[:], h.Sum(nil)) 240 | return r 241 | } 242 | -------------------------------------------------------------------------------- /dialing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "sync/atomic" 10 | 11 | "github.com/davidlazar/go-crypto/encoding/base32" 12 | 13 | "vuvuzela.io/alpenhorn/addfriend" 14 | "vuvuzela.io/alpenhorn/bloom" 15 | "vuvuzela.io/alpenhorn/config" 16 | "vuvuzela.io/alpenhorn/coordinator" 17 | "vuvuzela.io/alpenhorn/dialing" 18 | "vuvuzela.io/alpenhorn/errors" 19 | "vuvuzela.io/alpenhorn/log" 20 | "vuvuzela.io/alpenhorn/typesocket" 21 | "vuvuzela.io/crypto/onionbox" 22 | "vuvuzela.io/vuvuzela/mixnet" 23 | ) 24 | 25 | type dialingRoundState struct { 26 | Round uint32 27 | Config *config.DialingConfig 28 | ConfigParent *config.SignedConfig 29 | } 30 | 31 | func (c *Client) dialingMux() typesocket.Mux { 32 | return typesocket.NewMux(map[string]interface{}{ 33 | "newround": c.newDialingRound, 34 | "mix": c.sendDialingOnion, 35 | "mailbox": c.scanBloomFilter, 36 | "error": c.dialingRoundError, 37 | }) 38 | } 39 | 40 | func (c *Client) dialingRoundError(conn typesocket.Conn, v coordinator.RoundError) { 41 | log.WithFields(log.Fields{"round": v.Round}).Errorf("error from dialing coordinator: %s", v.Err) 42 | } 43 | 44 | func (c *Client) newDialingRound(conn typesocket.Conn, v coordinator.NewRound) { 45 | c.mu.Lock() 46 | defer c.mu.Unlock() 47 | 48 | st, ok := c.dialingRounds[v.Round] 49 | if ok { 50 | if st.ConfigParent.Hash() != v.ConfigHash { 51 | c.Handler.Error(errors.New("coordinator announced different configs round %d", v.Round)) 52 | } 53 | return 54 | } 55 | 56 | // common case 57 | if v.ConfigHash == c.dialingConfigHash { 58 | c.dialingRounds[v.Round] = &dialingRoundState{ 59 | Round: v.Round, 60 | Config: c.dialingConfig.Inner.(*config.DialingConfig), 61 | ConfigParent: c.dialingConfig, 62 | } 63 | return 64 | } 65 | 66 | configs, err := c.ConfigClient.FetchAndVerifyChain(c.dialingConfig, v.ConfigHash) 67 | if err != nil { 68 | c.Handler.Error(errors.Wrap(err, "fetching dialing config")) 69 | return 70 | } 71 | 72 | c.Handler.NewConfig(configs) 73 | 74 | newConfig := configs[0] 75 | c.dialingConfig = newConfig 76 | c.dialingConfigHash = v.ConfigHash 77 | 78 | if err := c.persistLocked(); err != nil { 79 | panic("failed to persist state: " + err.Error()) 80 | } 81 | 82 | c.dialingRounds[v.Round] = &dialingRoundState{ 83 | Round: v.Round, 84 | Config: newConfig.Inner.(*config.DialingConfig), 85 | ConfigParent: newConfig, 86 | } 87 | } 88 | 89 | func (c *Client) sendDialingOnion(conn typesocket.Conn, v coordinator.MixRound) { 90 | round := v.MixSettings.Round 91 | 92 | c.mu.Lock() 93 | st, ok := c.dialingRounds[round] 94 | c.mu.Unlock() 95 | if !ok { 96 | c.Handler.Error(errors.New("sendDialingOnion: round %d not configured", round)) 97 | return 98 | } 99 | 100 | serviceData := new(addfriend.ServiceData) 101 | if err := serviceData.Unmarshal(v.MixSettings.RawServiceData); err != nil { 102 | c.Handler.Error(errors.New("sendAddFriendOnion: round %d: error parsing service data: %s", round, err)) 103 | return 104 | } 105 | settingsMsg := v.MixSettings.SigningMessage() 106 | 107 | for i, mixer := range st.Config.MixServers { 108 | if !ed25519.Verify(mixer.Key, settingsMsg, v.MixSignatures[i]) { 109 | err := errors.New( 110 | "round %d: failed to verify mixnet settings for key %s", 111 | round, base32.EncodeToString(mixer.Key), 112 | ) 113 | c.Handler.Error(err) 114 | return 115 | } 116 | } 117 | 118 | atomic.StoreUint32(&c.lastDialingRound, round) 119 | 120 | mixMessage := new(dialing.MixMessage) 121 | call := c.nextOutgoingCall(round) 122 | // TODO timing leak 123 | if call != nil { 124 | c.mu.Lock() 125 | call.sentRound = round 126 | c.mu.Unlock() 127 | 128 | // Let the application know we're sending the call. 129 | c.Handler.SendingCall(call) 130 | 131 | token := call.computeKeys().token 132 | copy(mixMessage.Token[:], token[:]) 133 | mixMessage.Mailbox = usernameToMailbox(call.Username, serviceData.NumMailboxes) 134 | } else { 135 | // Send cover traffic. 136 | mixMessage.Mailbox = 0 137 | } 138 | 139 | onion, _ := onionbox.Seal(mustMarshal(mixMessage), mixnet.ForwardNonce(round), v.MixSettings.OnionKeys) 140 | 141 | // respond to the entry server with our onion for this round 142 | omsg := coordinator.OnionMsg{ 143 | Round: round, 144 | Onion: onion, 145 | } 146 | conn.Send("onion", omsg) 147 | } 148 | 149 | func (c *Client) nextOutgoingCall(round uint32) *OutgoingCall { 150 | c.mu.Lock() 151 | defer c.mu.Unlock() 152 | 153 | var call *OutgoingCall 154 | if len(c.outgoingCalls) > 0 { 155 | call = c.outgoingCalls[0] 156 | c.outgoingCalls = c.outgoingCalls[1:] 157 | } 158 | 159 | return call 160 | } 161 | 162 | func (c *Client) scanBloomFilter(conn typesocket.Conn, v coordinator.MailboxURL) { 163 | c.mu.Lock() 164 | st, ok := c.dialingRounds[v.Round] 165 | c.mu.Unlock() 166 | if !ok { 167 | return 168 | } 169 | 170 | mailboxID := usernameToMailbox(c.Username, v.NumMailboxes) 171 | mailbox, err := c.fetchMailbox(st.Config.CDNServer, v.URL, mailboxID) 172 | if err != nil { 173 | c.Handler.Error(errors.Wrap(err, "fetching mailbox")) 174 | return 175 | } 176 | 177 | filter := new(bloom.Filter) 178 | if err := filter.UnmarshalBinary(mailbox); err != nil { 179 | c.Handler.Error(errors.Wrap(err, "decoding bloom filter")) 180 | } 181 | 182 | allTokens := c.wheel.IncomingDialTokens(c.Username, v.Round, IntentMax) 183 | for _, user := range allTokens { 184 | for intent, token := range user.Tokens { 185 | if filter.Test(token[:]) { 186 | call := &IncomingCall{ 187 | Username: user.FromUsername, 188 | Intent: intent, 189 | SessionKey: c.wheel.SessionKey(user.FromUsername, v.Round), 190 | } 191 | c.Handler.ReceivedCall(call) 192 | } 193 | } 194 | } 195 | c.wheel.EraseKeys(v.Round) 196 | if err := c.persistKeywheel(); err != nil { 197 | panic(err) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /addfriend/mixer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package addfriend provides functionality for Alpenhorn's add-friend protocol. 6 | package addfriend 7 | 8 | import ( 9 | "bytes" 10 | "crypto/ed25519" 11 | "encoding/binary" 12 | "encoding/gob" 13 | "encoding/json" 14 | "fmt" 15 | "io/ioutil" 16 | "net/http" 17 | "strconv" 18 | "sync" 19 | "unsafe" 20 | 21 | "vuvuzela.io/alpenhorn/edhttp" 22 | "vuvuzela.io/alpenhorn/errors" 23 | "vuvuzela.io/concurrency" 24 | "vuvuzela.io/crypto/bn256" 25 | "vuvuzela.io/crypto/ibe" 26 | "vuvuzela.io/crypto/onionbox" 27 | "vuvuzela.io/crypto/rand" 28 | "vuvuzela.io/crypto/shuffle" 29 | "vuvuzela.io/vuvuzela/mixnet" 30 | ) 31 | 32 | const ( 33 | // SizeIntro is the size in bytes of an add-friend introduction. 34 | // This should be equal to int(unsafe.Sizeof(introduction{})) in 35 | // the alpenhorn package. 36 | SizeIntro = 228 37 | 38 | // SizeEncryptedIntro is the size of an encrypted introduction. 39 | SizeEncryptedIntro = SizeIntro + ibe.Overhead 40 | 41 | sizeMixMessage = int(unsafe.Sizeof(MixMessage{})) 42 | ) 43 | 44 | type MixMessage struct { 45 | Mailbox uint32 46 | EncryptedIntro [SizeEncryptedIntro]byte 47 | } 48 | 49 | type Mixer struct { 50 | SigningKey ed25519.PrivateKey 51 | 52 | Laplace rand.Laplace 53 | 54 | once sync.Once 55 | cdnClient *edhttp.Client 56 | } 57 | 58 | func (srv *Mixer) Bidirectional() bool { 59 | return false 60 | } 61 | 62 | func (srv *Mixer) SizeIncomingMessage() int { 63 | return sizeMixMessage 64 | } 65 | 66 | func (srv *Mixer) SizeReplyMessage() int { 67 | return -1 // only used in bidirectional mode 68 | } 69 | 70 | type ServiceData struct { 71 | CDNKey ed25519.PublicKey 72 | CDNAddress string 73 | NumMailboxes uint32 74 | } 75 | 76 | const AddFriendServiceDataVersion = 0 77 | 78 | func (srv *Mixer) ParseServiceData(data []byte) (interface{}, error) { 79 | d := new(ServiceData) 80 | err := d.Unmarshal(data) 81 | return d, err 82 | } 83 | 84 | func (srv *Mixer) GenerateNoise(settings mixnet.RoundSettings, myPos int) [][]byte { 85 | noiseTotal := uint32(0) 86 | noiseCounts := make([]uint32, settings.ServiceData.(*ServiceData).NumMailboxes+1) 87 | for b := range noiseCounts { 88 | bmu := srv.Laplace.Uint32() 89 | noiseCounts[b] = bmu 90 | noiseTotal += bmu 91 | } 92 | noise := make([][]byte, noiseTotal) 93 | 94 | mailbox := make([]uint32, len(noise)) 95 | idx := 0 96 | for b, count := range noiseCounts { 97 | for i := uint32(0); i < count; i++ { 98 | mailbox[idx] = uint32(b) 99 | idx++ 100 | } 101 | } 102 | 103 | nextServerKeys := settings.OnionKeys[myPos+1:] 104 | 105 | concurrency.ParallelFor(len(noise), func(p *concurrency.P) { 106 | for i, ok := p.Next(); ok; i, ok = p.Next() { 107 | var msg [sizeMixMessage]byte 108 | binary.BigEndian.PutUint32(msg[0:4], mailbox[i]) 109 | if mailbox[i] != 0 { 110 | // generate a valid-looking ciphertext 111 | encintro := msg[4:] 112 | rand.Read(encintro) 113 | g1 := new(bn256.G1).HashToPoint(encintro[:32]) 114 | copy(encintro, g1.Marshal()) 115 | } 116 | onion, _ := onionbox.Seal(msg[:], mixnet.ForwardNonce(settings.Round), nextServerKeys) 117 | noise[i] = onion 118 | } 119 | }) 120 | 121 | return noise 122 | } 123 | 124 | func (srv *Mixer) HandleMessages(settings mixnet.RoundSettings, messages [][]byte) (interface{}, error) { 125 | srv.once.Do(func() { 126 | srv.cdnClient = &edhttp.Client{ 127 | Key: srv.SigningKey, 128 | } 129 | }) 130 | 131 | serviceData := settings.ServiceData.(*ServiceData) 132 | 133 | // The last server doesn't shuffle by default, so shuffle here. 134 | shuffler := shuffle.New(rand.Reader, len(messages)) 135 | shuffler.Shuffle(messages) 136 | 137 | mailboxes := make(map[string][]byte) 138 | 139 | mx := new(MixMessage) 140 | for _, m := range messages { 141 | if len(m) != sizeMixMessage { 142 | continue 143 | } 144 | if err := mx.UnmarshalBinary(m); err != nil { 145 | continue 146 | } 147 | if mx.Mailbox == 0 { 148 | continue // dummy dead drop 149 | } 150 | mstr := strconv.FormatUint(uint64(mx.Mailbox), 10) 151 | mailboxes[mstr] = append(mailboxes[mstr], mx.EncryptedIntro[:]...) 152 | } 153 | 154 | buf := new(bytes.Buffer) 155 | err := gob.NewEncoder(buf).Encode(mailboxes) 156 | if err != nil { 157 | return "", errors.Wrap(err, "gob.Encode") 158 | } 159 | 160 | putURL := fmt.Sprintf("https://%s/put?bucket=%s/%d", serviceData.CDNAddress, settings.Service, settings.Round) 161 | resp, err := srv.cdnClient.Post(serviceData.CDNKey, putURL, "application/octet-stream", buf) 162 | if err != nil { 163 | return "", err 164 | } 165 | defer resp.Body.Close() 166 | if resp.StatusCode != http.StatusOK { 167 | msg, _ := ioutil.ReadAll(resp.Body) 168 | err = errors.New("bad CDN response: %s: %q", resp.Status, msg) 169 | return "", err 170 | } 171 | 172 | getURL := fmt.Sprintf("https://%s/get?bucket=%s/%d", serviceData.CDNAddress, settings.Service, settings.Round) 173 | return getURL, nil 174 | } 175 | 176 | func (m *MixMessage) MarshalBinary() ([]byte, error) { 177 | buf := new(bytes.Buffer) 178 | if err := binary.Write(buf, binary.BigEndian, m); err != nil { 179 | return nil, err 180 | } 181 | return buf.Bytes(), nil 182 | } 183 | 184 | func (m *MixMessage) UnmarshalBinary(data []byte) error { 185 | buf := bytes.NewReader(data) 186 | return binary.Read(buf, binary.BigEndian, m) 187 | } 188 | 189 | func (d *ServiceData) Unmarshal(data []byte) error { 190 | if len(data) == 0 { 191 | return errors.New("empty raw service data") 192 | } 193 | if data[0] != AddFriendServiceDataVersion { 194 | return errors.New("invalid version: %d", data[0]) 195 | } 196 | return json.Unmarshal(data[1:], d) 197 | } 198 | 199 | func (d ServiceData) Marshal() []byte { 200 | buf := new(bytes.Buffer) 201 | buf.WriteByte(AddFriendServiceDataVersion) 202 | err := json.NewEncoder(buf).Encode(d) 203 | if err != nil { 204 | panic(err) 205 | } 206 | return buf.Bytes() 207 | } 208 | -------------------------------------------------------------------------------- /pkg/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package pkg 6 | 7 | import ( 8 | "bytes" 9 | "crypto/ed25519" 10 | "crypto/rand" 11 | "encoding/json" 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | 16 | "golang.org/x/crypto/nacl/box" 17 | 18 | "vuvuzela.io/alpenhorn/edhttp" 19 | "vuvuzela.io/alpenhorn/errors" 20 | "vuvuzela.io/crypto/bls" 21 | "vuvuzela.io/crypto/ibe" 22 | ) 23 | 24 | // A Client connects to a PKG server to extract private keys. 25 | // Before a client can extract keys, it must register the username 26 | // and login key with the PKG server. The client must then verify 27 | // ownership of the username, unless the PKG server is running in 28 | // first-come-first-serve mode. 29 | type Client struct { 30 | // Username is identity in Identity-Based Encryption. 31 | Username string 32 | 33 | // LoginKey is used to authenticate to the PKG server. 34 | LoginKey ed25519.PrivateKey 35 | 36 | // UserLongTermKey is the user's long-term signing key. The 37 | // PKG server attests to this key during extraction. 38 | UserLongTermKey ed25519.PublicKey 39 | 40 | HTTPClient *edhttp.Client 41 | } 42 | 43 | // Register attempts to register the client's username and login key 44 | // with the PKG server. It only needs to be called once per PKG server. 45 | func (c *Client) Register(server PublicServerConfig, token string) error { 46 | loginPublicKey := c.LoginKey.Public() 47 | args := ®isterArgs{ 48 | Username: c.Username, 49 | LoginKey: loginPublicKey.(ed25519.PublicKey), 50 | RegistrationToken: token, 51 | } 52 | 53 | var reply string 54 | err := c.do(server, "register", args, &reply) 55 | if err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func (c *Client) CheckStatus(server PublicServerConfig) error { 62 | args := &statusArgs{ 63 | Username: c.Username, 64 | ServerSigningKey: server.Key, 65 | } 66 | rand.Read(args.Message[:]) 67 | args.Signature = ed25519.Sign(c.LoginKey, args.msg()) 68 | 69 | var reply statusReply 70 | err := c.do(server, "status", args, &reply) 71 | if err != nil { 72 | return err 73 | } 74 | return nil 75 | } 76 | 77 | type ExtractResult struct { 78 | PrivateKey *ibe.IdentityPrivateKey 79 | IdentitySig bls.Signature 80 | } 81 | 82 | // Extract obtains the user's IBE private key for the given round from the PKG. 83 | func (c *Client) Extract(server PublicServerConfig, round uint32) (*ExtractResult, error) { 84 | myPub, myPriv, err := box.GenerateKey(rand.Reader) 85 | if err != nil { 86 | panic("box.GenerateKey: " + err.Error()) 87 | } 88 | 89 | args := &extractArgs{ 90 | Round: round, 91 | Username: c.Username, 92 | ReturnKey: myPub, 93 | UserLongTermKey: c.UserLongTermKey, 94 | ServerSigningKey: server.Key, 95 | } 96 | args.Sign(c.LoginKey) 97 | 98 | reply := new(extractReply) 99 | err = c.do(server, "extract", args, reply) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if reply.Round != round { 105 | return nil, errors.New("expected reply for round %d, but got %d", round, reply.Round) 106 | } 107 | if reply.Username != c.Username { 108 | return nil, errors.New("expected reply for username %q, but got %q", c.Username, reply.Username) 109 | } 110 | if l := len(reply.EncryptedPrivateKey); l < 32 { 111 | return nil, errors.New("unexpectedly short ciphertext (%d bytes)", l) 112 | } 113 | if !reply.Verify(server.Key) { 114 | return nil, errors.New("invalid signature") 115 | } 116 | // TODO un-hardcode 64 117 | if len(reply.IdentitySig) != 64 { 118 | return nil, errors.New("invalid identity signature: got %d bytes, want %d", len(reply.IdentitySig), 64) 119 | } 120 | 121 | theirPub := new([32]byte) 122 | copy(theirPub[:], reply.EncryptedPrivateKey[0:32]) 123 | ctxt := reply.EncryptedPrivateKey[32:] 124 | msg, ok := box.Open(nil, ctxt, new([24]byte), theirPub, myPriv) 125 | if !ok { 126 | return nil, errors.New("box authentication failed") 127 | } 128 | 129 | ibeKey := new(ibe.IdentityPrivateKey) 130 | if err := ibeKey.UnmarshalBinary(msg); err != nil { 131 | return nil, errors.Wrap(err, "unmarshalling ibe identity key") 132 | } 133 | 134 | return &ExtractResult{ 135 | PrivateKey: ibeKey, 136 | IdentitySig: reply.IdentitySig, 137 | }, nil 138 | } 139 | 140 | func (c *Client) do(server PublicServerConfig, path string, args, reply interface{}) error { 141 | req := &pkgRequest{ 142 | PublicServerConfig: server, 143 | Path: path, 144 | Args: args, 145 | Reply: reply, 146 | Client: c.HTTPClient, 147 | TweakRequest: func(req *http.Request) { 148 | // We're running addfriend faster, so keep connection alive for now. 149 | //req.Close = true 150 | }, 151 | } 152 | 153 | return req.Do() 154 | } 155 | 156 | type pkgRequest struct { 157 | PublicServerConfig 158 | 159 | Path string 160 | Args interface{} 161 | Reply interface{} 162 | Client *edhttp.Client 163 | 164 | TweakRequest func(*http.Request) 165 | } 166 | 167 | func (req *pkgRequest) Do() error { 168 | buf := new(bytes.Buffer) 169 | if err := json.NewEncoder(buf).Encode(req.Args); err != nil { 170 | return errors.Wrap(err, "json.Encode") 171 | } 172 | 173 | url := fmt.Sprintf("https://%s/%s", req.PublicServerConfig.Address, req.Path) 174 | httpReq, err := http.NewRequest("POST", url, buf) 175 | if err != nil { 176 | return err 177 | } 178 | if req.TweakRequest != nil { 179 | req.TweakRequest(httpReq) 180 | } 181 | 182 | resp, err := req.Client.Do(req.PublicServerConfig.Key, httpReq) 183 | if err != nil { 184 | return err 185 | } 186 | defer resp.Body.Close() 187 | body, err := ioutil.ReadAll(resp.Body) 188 | if err != nil { 189 | return errors.Wrap(err, "reading http response body") 190 | } 191 | if resp.StatusCode == http.StatusOK { 192 | if err := json.Unmarshal(body, req.Reply); err != nil { 193 | return errors.Wrap(err, "json.Unmarshal") 194 | } 195 | return nil 196 | } else { 197 | var pkgErr Error 198 | if err := json.Unmarshal(body, &pkgErr); err != nil { 199 | return errors.New( 200 | "error response (%s) with unparseable body: %q", 201 | resp.Status, body, 202 | ) 203 | } 204 | return pkgErr 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /cmd/alpenhorn-pkg/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "crypto/ed25519" 11 | "flag" 12 | "fmt" 13 | "io/ioutil" 14 | golog "log" 15 | "net/http" 16 | "os" 17 | "os/signal" 18 | "path/filepath" 19 | "syscall" 20 | "text/template" 21 | "time" 22 | 23 | "vuvuzela.io/alpenhorn/cmd/cmdutil" 24 | "vuvuzela.io/alpenhorn/config" 25 | "vuvuzela.io/alpenhorn/edtls" 26 | "vuvuzela.io/alpenhorn/encoding/toml" 27 | "vuvuzela.io/alpenhorn/errors" 28 | "vuvuzela.io/alpenhorn/internal/alplog" 29 | "vuvuzela.io/alpenhorn/log" 30 | "vuvuzela.io/alpenhorn/pkg" 31 | "vuvuzela.io/crypto/rand" 32 | ) 33 | 34 | var ( 35 | doinit = flag.Bool("init", false, "create config file") 36 | persistPath = flag.String("persist", "persist_pkg", "persistent data directory") 37 | ) 38 | 39 | type Config struct { 40 | PublicKey ed25519.PublicKey 41 | PrivateKey ed25519.PrivateKey 42 | 43 | ListenAddr string 44 | } 45 | 46 | var funcMap = template.FuncMap{ 47 | "base32": toml.EncodeBytes, 48 | } 49 | 50 | const confTemplate = `# Alpenhorn PKG server config 51 | 52 | publicKey = {{.PublicKey | base32 | printf "%q"}} 53 | privateKey = {{.PrivateKey | base32 | printf "%q"}} 54 | 55 | listenAddr = {{.ListenAddr | printf "%q"}} 56 | ` 57 | 58 | func writeNewConfig(path string) { 59 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | conf := &Config{ 65 | PublicKey: publicKey, 66 | PrivateKey: privateKey, 67 | 68 | ListenAddr: "0.0.0.0:80", 69 | } 70 | 71 | tmpl := template.Must(template.New("config").Funcs(funcMap).Parse(confTemplate)) 72 | 73 | buf := new(bytes.Buffer) 74 | err = tmpl.Execute(buf, conf) 75 | if err != nil { 76 | log.Fatalf("template error: %s", err) 77 | } 78 | data := buf.Bytes() 79 | 80 | err = ioutil.WriteFile(path, data, 0600) 81 | if err != nil { 82 | log.Fatal(err) 83 | } 84 | fmt.Printf("wrote %s\n", path) 85 | } 86 | 87 | func main() { 88 | flag.Parse() 89 | 90 | if err := os.MkdirAll(*persistPath, 0700); err != nil { 91 | log.Fatal(err) 92 | return 93 | } 94 | confPath := filepath.Join(*persistPath, "pkg.conf") 95 | 96 | if *doinit { 97 | if cmdutil.Overwrite(confPath) { 98 | writeNewConfig(confPath) 99 | } 100 | return 101 | } 102 | 103 | data, err := ioutil.ReadFile(confPath) 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | conf := new(Config) 108 | err = toml.Unmarshal(data, conf) 109 | if err != nil { 110 | log.Fatalf("error parsing config %q: %s", confPath, err) 111 | } 112 | err = checkConfig(conf) 113 | if err != nil { 114 | log.Fatalf("invalid config: %s", err) 115 | } 116 | 117 | logsDir := filepath.Join(*persistPath, "logs") 118 | logHandler, err := alplog.NewProductionOutput(logsDir) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | signedConfig, err := config.StdClient.CurrentConfig("AddFriend") 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | addFriendConfig := signedConfig.Inner.(*config.AddFriendConfig) 128 | if addFriendConfig.Registrar.Address == "" { 129 | log.Fatal("no Registrar Address defined in current addfriend config!") 130 | } 131 | 132 | dbPath := filepath.Join(*persistPath, "db") 133 | if err := os.MkdirAll(dbPath, 0700); err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | pkgConfig := &pkg.Config{ 138 | DBPath: dbPath, 139 | SigningKey: conf.PrivateKey, 140 | 141 | CoordinatorKey: addFriendConfig.Coordinator.Key, 142 | RegistrarKey: addFriendConfig.Registrar.Key, 143 | 144 | Logger: &log.Logger{ 145 | Level: log.InfoLevel, 146 | EntryHandler: logHandler, 147 | }, 148 | 149 | RegTokenHandler: pkg.ExternalVerifier(fmt.Sprintf("https://%s/verify", addFriendConfig.Registrar.Address)), 150 | } 151 | pkgServer, err := pkg.NewServer(pkgConfig) 152 | if err != nil { 153 | log.Fatalf("pkg.NewServer: %s", err) 154 | } 155 | defer func() { 156 | err := pkgServer.Close() 157 | if err != nil { 158 | log.Infof("PKG closed with error: %s", err) 159 | } 160 | }() 161 | 162 | errorLogPath := filepath.Join(*persistPath, "http_errors.log") 163 | errorFile, err := os.OpenFile(errorLogPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660) 164 | if err != nil { 165 | log.Fatal(err) 166 | } 167 | defer errorFile.Close() 168 | errorLog := golog.New(errorFile, "", golog.LstdFlags|golog.LUTC|golog.Lshortfile) 169 | 170 | httpServer := &http.Server{ 171 | Handler: pkgServer, 172 | ErrorLog: errorLog, 173 | 174 | ReadTimeout: 10 * time.Second, 175 | WriteTimeout: 30 * time.Second, 176 | IdleTimeout: 60 * time.Second, 177 | } 178 | 179 | sigChan := make(chan os.Signal, 1) 180 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 181 | shutdownDone := make(chan struct{}) 182 | go func() { 183 | <-sigChan 184 | log.Infof("Shutting down...") 185 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 186 | defer cancel() 187 | 188 | err := httpServer.Shutdown(ctx) 189 | if err != nil { 190 | log.Infof("HTTP server shutdown with error: %s", err) 191 | } 192 | close(shutdownDone) 193 | }() 194 | 195 | listener, err := edtls.Listen("tcp", conf.ListenAddr, conf.PrivateKey) 196 | if err != nil { 197 | log.Fatalf("edtls.Listen: %s", err) 198 | } 199 | 200 | // Let the user know what's happening before switching the logger. 201 | log.Infof("Listening on %q; logging to %s", conf.ListenAddr, logHandler.Name()) 202 | // Record the start time in the logs directory. 203 | pkgConfig.Logger.Infof("Listening on %q", conf.ListenAddr) 204 | 205 | err = httpServer.Serve(listener) 206 | if err != http.ErrServerClosed { 207 | log.Errorf("http listen: %s", err) 208 | } 209 | 210 | <-shutdownDone 211 | } 212 | 213 | func checkConfig(conf *Config) error { 214 | if conf.ListenAddr == "" { 215 | return errors.New("no listen address specified") 216 | } 217 | if len(conf.PrivateKey) != ed25519.PrivateKeySize { 218 | return errors.New("invalid private key") 219 | } 220 | expectedPub := conf.PrivateKey.Public().(ed25519.PublicKey) 221 | if !bytes.Equal(expectedPub, conf.PublicKey) { 222 | return errors.New("public key does not correspond to private key") 223 | } 224 | return nil 225 | } 226 | -------------------------------------------------------------------------------- /friend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 David Lazar. All rights reserved. 2 | // Use of this source code is governed by the GNU AGPL 3 | // license that can be found in the LICENSE file. 4 | 5 | package alpenhorn 6 | 7 | import ( 8 | "crypto/ed25519" 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | // Friend is an entry in the client's address book. 14 | type Friend struct { 15 | Username string 16 | LongTermKey ed25519.PublicKey 17 | 18 | // extraData stores application-specific data. 19 | extraData []byte 20 | client *Client 21 | } 22 | 23 | // GetFriends returns all the friends in the client's address book. 24 | func (c *Client) GetFriends() []*Friend { 25 | c.mu.Lock() 26 | fs := make([]*Friend, 0, len(c.friends)) 27 | for _, friend := range c.friends { 28 | fs = append(fs, friend) 29 | } 30 | c.mu.Unlock() 31 | return fs 32 | } 33 | 34 | // GetFriend returns the friend object for the given username, 35 | // or nil if username is not in the client's address book. 36 | func (c *Client) GetFriend(username string) *Friend { 37 | c.mu.Lock() 38 | friend := c.friends[username] 39 | c.mu.Unlock() 40 | return friend 41 | } 42 | 43 | // Remove removes the friend from the client's address book. 44 | func (f *Friend) Remove() error { 45 | f.client.mu.Lock() 46 | defer f.client.mu.Unlock() 47 | 48 | delete(f.client.friends, f.Username) 49 | f.client.wheel.Remove(f.Username) 50 | 51 | // delete any outgoing calls for this friend 52 | calls := f.client.outgoingCalls[:0] 53 | for _, call := range f.client.outgoingCalls { 54 | if call.Username != f.Username { 55 | calls = append(calls, call) 56 | } 57 | } 58 | f.client.outgoingCalls = calls 59 | 60 | err := f.client.persistLocked() 61 | return err 62 | } 63 | 64 | // SetExtraData overwrites the friend's extra data field with the given 65 | // data. The extra data field is useful for application-specific data 66 | // about the friend, such as additional contact info, notes, or a photo. 67 | // 68 | // Applications should use the extra data field to store information 69 | // about friends instead of maintaining a separate friend list because 70 | // the Alpenhorn client will (eventually) ensure that the size of the 71 | // persisted data on disk does not leak metadata. 72 | func (f *Friend) SetExtraData(data []byte) error { 73 | f.client.mu.Lock() 74 | f.extraData = make([]byte, len(data)) 75 | copy(f.extraData, data) 76 | err := f.client.persistLocked() 77 | f.client.mu.Unlock() 78 | return err 79 | } 80 | 81 | // ExtraData returns a copy of the extra data field for the friend. 82 | func (f *Friend) ExtraData() []byte { 83 | f.client.mu.Lock() 84 | data := make([]byte, len(f.extraData)) 85 | copy(data, f.extraData) 86 | f.client.mu.Unlock() 87 | return data 88 | } 89 | 90 | // UnsafeKeywheelState exposes the internal keywheel state for this friend. 91 | // This should only be used for debugging. 92 | func (f *Friend) UnsafeKeywheelState() (uint32, *[32]byte) { 93 | return f.client.wheel.UnsafeGet(f.Username) 94 | } 95 | 96 | // SessionKey returns the shared key at the given round. 97 | // This should only be used for debugging. 98 | func (f *Friend) SessionKey(round uint32) *[32]byte { 99 | return f.client.wheel.SessionKey(f.Username, round) 100 | } 101 | 102 | // Intents are the dialing intents passed to Call. 103 | const IntentMax = 3 104 | 105 | // Call is used to call a friend using Alpenhorn's dialing protocol. 106 | // Call does not send the call right away but queues the call for an 107 | // upcoming dialing round. The resulting OutgoingCall is the queued 108 | // call object. Call does nothing and returns nil if the friend is 109 | // not in the client's address book. 110 | func (f *Friend) Call(intent int) *OutgoingCall { 111 | if intent >= IntentMax { 112 | panic(fmt.Sprintf("invalid intent: %d", intent)) 113 | } 114 | if !f.client.wheel.Exists(f.Username) { 115 | return nil 116 | } 117 | 118 | call := &OutgoingCall{ 119 | Username: f.Username, 120 | Created: time.Now(), 121 | client: f.client, 122 | intent: intent, 123 | } 124 | f.client.mu.Lock() 125 | f.client.outgoingCalls = append(f.client.outgoingCalls, call) 126 | f.client.mu.Unlock() 127 | return call 128 | } 129 | 130 | type IncomingCall struct { 131 | Username string 132 | Intent int 133 | SessionKey *[32]byte 134 | } 135 | 136 | type OutgoingCall struct { 137 | Username string 138 | Created time.Time 139 | 140 | client *Client 141 | intent int 142 | sentRound uint32 143 | dialToken *[32]byte 144 | sessionKey *[32]byte 145 | } 146 | 147 | // Sent returns true if the call has been sent and false otherwise. 148 | func (r *OutgoingCall) Sent() bool { 149 | r.client.mu.Lock() 150 | sent := r.sentRound != 0 151 | r.client.mu.Unlock() 152 | return sent 153 | } 154 | 155 | func (r *OutgoingCall) Intent() int { 156 | r.client.mu.Lock() 157 | intent := r.intent 158 | r.client.mu.Unlock() 159 | return intent 160 | } 161 | 162 | func (r *OutgoingCall) UpdateIntent(intent int) error { 163 | r.client.mu.Lock() 164 | defer r.client.mu.Unlock() 165 | if r.dialToken != nil { 166 | return ErrTooLate 167 | } 168 | r.intent = intent 169 | return nil 170 | } 171 | 172 | type computeKeysResult struct{ token, sessionKey *[32]byte } 173 | 174 | func (r *OutgoingCall) computeKeys() computeKeysResult { 175 | r.client.mu.Lock() 176 | if r.sentRound == 0 || r.dialToken != nil { 177 | r.client.mu.Unlock() 178 | return computeKeysResult{ 179 | token: r.dialToken, 180 | sessionKey: r.sessionKey, 181 | } 182 | } 183 | intent := r.intent 184 | round := r.sentRound 185 | r.client.mu.Unlock() 186 | 187 | dialToken := r.client.wheel.OutgoingDialToken(r.Username, round, intent) 188 | sessionKey := r.client.wheel.SessionKey(r.Username, round) 189 | 190 | r.client.mu.Lock() 191 | defer r.client.mu.Unlock() 192 | 193 | if r.dialToken != nil { 194 | return computeKeysResult{ 195 | token: r.dialToken, 196 | sessionKey: r.sessionKey, 197 | } 198 | } 199 | 200 | r.intent = intent 201 | r.dialToken = dialToken 202 | r.sessionKey = sessionKey 203 | return computeKeysResult{ 204 | token: r.dialToken, 205 | sessionKey: r.sessionKey, 206 | } 207 | } 208 | 209 | // SessionKey returns the session key established for this call, 210 | // or nil if the call has not been sent yet. 211 | func (r *OutgoingCall) SessionKey() *[32]byte { 212 | return r.computeKeys().sessionKey 213 | } 214 | 215 | // Cancel removes the call from the outgoing queue, returning 216 | // ErrTooLate if the call is not found in the queue. 217 | func (r *OutgoingCall) Cancel() error { 218 | r.client.mu.Lock() 219 | defer r.client.mu.Unlock() 220 | 221 | calls := r.client.outgoingCalls 222 | index := -1 223 | for i, c := range calls { 224 | if r == c { 225 | index = i 226 | } 227 | } 228 | 229 | if index == -1 { 230 | return ErrTooLate 231 | } 232 | 233 | r.client.outgoingCalls = append(calls[:index], calls[index+1:]...) 234 | return nil 235 | } 236 | --------------------------------------------------------------------------------