Skip to content

Commit

Permalink
feat: e2e encrypted stream
Browse files Browse the repository at this point in the history
  • Loading branch information
jsiebens committed Apr 12, 2022
1 parent 7b229cb commit acbfdb1
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 27 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/mitchellh/pointerstructure v1.2.1
github.com/mr-tron/base58 v1.2.0
github.com/muesli/coral v1.0.0
github.com/nknorg/encrypted-stream v1.0.1
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/rancher/remotedialer v0.2.6-0.20220107175045-b2d660c628d5
github.com/sirupsen/logrus v1.8.1
Expand All @@ -45,6 +46,7 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/imdario/mergo v0.3.9 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect
github.com/kr/pretty v0.2.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
Expand Down Expand Up @@ -335,6 +337,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nknorg/encrypted-stream v1.0.1 h1:lyWouCwUY3WUOfYaoez0wNEudsZJ3qc9Knxxi4Ysjeo=
github.com/nknorg/encrypted-stream v1.0.1/go.mod h1:VXJDhlUoF3uJSFLwIWnRLkiX5QPFB3E8oe2EUBwPoU0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down Expand Up @@ -498,6 +502,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down
3 changes: 3 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const (
AuthHeader = "x-brink-auth"
KeyHeader = "x-brink-api-key"
TokenHeader = "x-brink-api-token"

UpgradeHeaderValue = "x-brink-protocol"
HandshakeHeaderName = "x-brink-handshake-key"
)

type CreateSessionRequest struct {
Expand Down
68 changes: 45 additions & 23 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/gorilla/websocket"
"github.com/jsiebens/brink/internal/api"
"github.com/jsiebens/brink/internal/key"
"github.com/jsiebens/brink/internal/util"
"github.com/rancher/remotedialer"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -48,11 +49,6 @@ func createClient(proxy, caFile string, insecureSkipVerify bool) (*Client, error
return nil, err
}

websocketBaseUrl, err := util.NormalizeWsUrl(proxy)
if err != nil {
return nil, err
}

if caFile != "" {
caCert, err := ioutil.ReadFile(caFile)
if err != nil {
Expand All @@ -70,34 +66,48 @@ func createClient(proxy, caFile string, insecureSkipVerify bool) (*Client, error
InsecureSkipVerify: insecureSkipVerify,
}

client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
proxyPublicKey, err := getProxyPublicKey(context.Background(), resty.NewWithClient(&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}), targetBaseUrl)
if err != nil {
return nil, err
}

clientPrivateKey, err := key.GeneratePrivateKey()
if err != nil {
return nil, err
}

dialer := &websocket.Dialer{
dialer := NewDialer(*proxyPublicKey, *clientPrivateKey, targetBaseUrl, tlsConfig)
client := &http.Client{
Transport: &http.Transport{
DialContext: dialer,
TLSClientConfig: tlsConfig,
},
}

websocketDialer := &websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: remotedialer.HandshakeTimeOut,
TLSClientConfig: tlsConfig,
NetDialContext: dialer,
}

c := &Client{
httpClient: resty.NewWithClient(client),
dialer: dialer,
httpBaseUrl: targetBaseUrl,
websocketBaseUrl: websocketBaseUrl,
httpClient: resty.NewWithClient(client),
dialer: websocketDialer,
}

return c, nil
}

type Client struct {
httpClient *resty.Client
dialer *websocket.Dialer
forwarder *Forwarder
httpBaseUrl string
websocketBaseUrl string
httpClient *resty.Client
dialer *websocket.Dialer
forwarder *Forwarder
target string
}

func (c *Client) authenticate(ctx context.Context) error {
_ = DeleteAuthToken(c.httpBaseUrl)
_ = DeleteAuthToken(c.target)

sn, err := c.createSession(ctx, "")
if err != nil {
Expand Down Expand Up @@ -126,7 +136,7 @@ func (c *Client) authenticate(ctx context.Context) error {
authToken = sn.AuthToken
}

err = StoreAuthToken(c.httpBaseUrl, authToken)
err = StoreAuthToken(c.target, authToken)
if err != nil {
fmt.Println()
fmt.Printf(" Unable to store auth token in your system credential store: %s\n", err)
Expand All @@ -143,7 +153,7 @@ func (c *Client) start(ctx context.Context) error {
return err
}

currentAuthToken, storeAuthToken, _ := LoadAuthToken(c.httpBaseUrl)
currentAuthToken, storeAuthToken, _ := LoadAuthToken(c.target)

sn, err := c.createSession(ctx, currentAuthToken)
if err != nil {
Expand Down Expand Up @@ -173,7 +183,7 @@ func (c *Client) start(ctx context.Context) error {
sessionToken = sn.SessionToken
}

_ = storeAuthToken(c.httpBaseUrl, authToken)
_ = storeAuthToken(c.target, authToken)

if err := c.connect(ctx, sn.SessionId, sessionToken); err != nil {
return err
Expand All @@ -199,7 +209,7 @@ func (c *Client) createSession(ctx context.Context, authToken string) (*api.Sess
SetResult(&result).
SetError(&errMsg).
SetContext(ctx).
Post(c.httpBaseUrl + "/p/session")
Post("http://internal/p/session")

if err != nil {
return nil, err
Expand Down Expand Up @@ -241,7 +251,7 @@ func (c *Client) checkSessionToken(ctx context.Context, sessionId string) (*api.
SetResult(&result).
SetError(&errMsg).
SetContext(ctx).
Post(c.httpBaseUrl + "/p/token")
Post("http://internal/p/token")

if err != nil {
return nil, err
Expand All @@ -259,7 +269,7 @@ func (c *Client) connect(ctx context.Context, id, token string) error {
headers.Add(api.IdHeader, id)
headers.Add(api.AuthHeader, token)

return c.connectToProxy(ctx, c.websocketBaseUrl+"/p/connect", headers)
return c.connectToProxy(ctx, "ws://internal/p/connect", headers)
}

func (c *Client) connectToProxy(rootCtx context.Context, proxyURL string, headers http.Header) error {
Expand Down Expand Up @@ -318,3 +328,15 @@ type serverError struct {
func (e serverError) Error() string {
return fmt.Sprintf("unexpected status code: %d - %s", e.code, e.message)
}

func getProxyPublicKey(ctx context.Context, client *resty.Client, httpBaseUrl string) (*key.PublicKey, error) {
resp, err := client.R().
SetContext(ctx).
Get(httpBaseUrl + "/p/key")

if err != nil {
return nil, err
}

return key.ParsePublicKey(resp.String())
}
88 changes: 88 additions & 0 deletions internal/client/dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package client

import (
"context"
"crypto/tls"
"errors"
"fmt"
"github.com/jsiebens/brink/internal/api"
"github.com/jsiebens/brink/internal/key"
"github.com/jsiebens/brink/internal/util"
stream "github.com/nknorg/encrypted-stream"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/url"
)

func NewDialer(proxyPublicKey key.PublicKey, clientPrivateKey key.PrivateKey, target string, tlsConfig *tls.Config) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
u, err := url.Parse(target)
if err != nil {
return nil, err
}

u.Path = "/p/upgrade"

tr := &http.Transport{
ForceAttemptHTTP2: false,
TLSClientConfig: tlsConfig,
TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
}

connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
connCh <- info.Conn
},
}
traceCtx := httptrace.WithClientTrace(ctx, &trace)
req := &http.Request{
Method: "GET",
URL: u,
Header: http.Header{
"Upgrade": []string{api.UpgradeHeaderValue},
"Connection": []string{"upgrade"},
api.HandshakeHeaderName: []string{clientPrivateKey.Public().String()},
},
}
req = req.WithContext(traceCtx)

resp, err := tr.RoundTrip(req)
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusSwitchingProtocols {
return nil, fmt.Errorf("unexpected HTTP response: %s", resp.Status)
}

var switchedConn net.Conn
select {
case switchedConn = <-connCh:
default:
}
if switchedConn == nil {
_ = resp.Body.Close()
return nil, fmt.Errorf("httptrace didn't provide a connection")
}

if next := resp.Header.Get("Upgrade"); next != api.UpgradeHeaderValue {
_ = resp.Body.Close()
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
}

rwc, ok := resp.Body.(io.ReadWriteCloser)
if !ok {
_ = resp.Body.Close()
return nil, errors.New("http Transport did not provide a writable body")
}

return stream.NewEncryptedStream(util.NewAltReadWriteCloserConn(rwc, switchedConn), &stream.Config{
Cipher: key.NewBoxCipher(clientPrivateKey, proxyPublicKey),
SequentialNonce: false, // only when key is unique for every stream
Initiator: true, // only on the dialer side
})
}
}
3 changes: 2 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ type Provider struct {
}

type Proxy struct {
Policies map[string]Policy `yaml:"policies"`
PrivateKey string `yaml:"private_key"`
Policies map[string]Policy `yaml:"policies"`
}

type Policy struct {
Expand Down
61 changes: 61 additions & 0 deletions internal/key/cipher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package key

import (
"fmt"
stream "github.com/nknorg/encrypted-stream"
"golang.org/x/crypto/nacl/box"
)

type BoxCipher struct {
privateKey *[32]byte
publicKey *[32]byte
}

func NewBoxCipher(pr PrivateKey, pu PublicKey) stream.Cipher {
return &BoxCipher{
privateKey: &pr.k,
publicKey: &pu.k,
}
}

// Encrypt implements Cipher.
func (c *BoxCipher) Encrypt(ciphertext, plaintext, nonce []byte) ([]byte, error) {
var n [24]byte
copy(n[:], nonce[:24])

encrypted := box.Seal(ciphertext[:0], plaintext, &n, c.publicKey, c.privateKey)

return ciphertext[:len(encrypted)], nil
}

// Decrypt implements Cipher.
func (c *BoxCipher) Decrypt(plaintext, ciphertext, nonce []byte) ([]byte, error) {
var n [24]byte
copy(n[:], nonce[:24])

plaintext, ok := box.Open(plaintext[:0], ciphertext, &n, c.publicKey, c.privateKey)
if !ok {
return nil, fmt.Errorf("decrypt failed")
}

return plaintext, nil
}

// MaxOverhead implements Cipher.
func (c *BoxCipher) MaxOverhead() int {
return box.Overhead
}

// NonceSize implements Cipher.
func (c *BoxCipher) NonceSize() int {
return 24
}

func parseKey(key string) (*[32]byte, error) {
k := new([32]byte)
err := parseHex(k[:], key)
if err != nil {
return nil, err
}
return k, nil
}
Loading

0 comments on commit acbfdb1

Please sign in to comment.