├── .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 |
--------------------------------------------------------------------------------