Skip to content

Commit

Permalink
Security: App tokens (#246)
Browse files Browse the repository at this point in the history
* App token flow ready. Tests WIP.

* Remove comment

* Add tests
  • Loading branch information
dmerrill6 authored Nov 14, 2020
1 parent bddaa05 commit ac7c728
Show file tree
Hide file tree
Showing 19 changed files with 1,691 additions and 417 deletions.
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func (a *App) Start(ctx context.Context) error {
srv := grpc.New(
sv,
fuseController,
kc,
grpc.WithPort(a.cfg.GetInt(config.SpaceServerPort, 0)),
grpc.WithProxyPort(a.cfg.GetInt(config.SpaceProxyServerPort, 0)),
grpc.WithRestProxyPort(a.cfg.GetInt(config.SpaceRestProxyServerPort, 0)),
Expand Down
80 changes: 80 additions & 0 deletions core/keychain/app_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package keychain

import (
"errors"

"github.com/99designs/keyring"
"github.com/FleekHQ/space-daemon/core/permissions"
)

const AppTokenStoreKey = "appToken"
const MasterAppTokenStoreKey = "masterAppToken"

var ErrMasterTokenAlreadyExists = errors.New("master app token already exists")

func (kc *keychain) StoreAppToken(tok *permissions.AppToken) error {
ring, err := kc.getKeyRing()
if err != nil {
return err
}

// Prevent overriding existing master key
key, _ := kc.st.Get([]byte(getMasterTokenStKey()))
if key != nil && tok.IsMaster {
return ErrMasterTokenAlreadyExists
}

// Prevents overriding even if user logged out and logged back in (which clears the store)
_, err = ring.Get(getMasterTokenStKey())
if err == nil && tok.IsMaster {
return ErrMasterTokenAlreadyExists
}

marshalled, err := permissions.MarshalToken(tok)
if err != nil {
return err
}

err = ring.Set(keyring.Item{
Key: AppTokenStoreKey + "_" + tok.Key,
Data: marshalled,
Label: "Space App - App Token",
})
if err != nil {
return err
}

if tok.IsMaster {
if err := kc.st.Set([]byte(getMasterTokenStKey()), []byte(tok.Key)); err != nil {
return err
}

if err := ring.Set(keyring.Item{
Key: getMasterTokenStKey(),
Data: marshalled,
Label: "Space App - Master App Token",
}); err != nil {
return err
}
}

return nil
}

func (kc *keychain) GetAppToken(key string) (*permissions.AppToken, error) {
ring, err := kc.getKeyRing()
if err != nil {
return nil, err
}

token, err := ring.Get(AppTokenStoreKey + "_" + key)
if err != nil {
return nil, err
}

return permissions.UnmarshalToken(token.Data)
}

func getMasterTokenStKey() string {
return AppTokenStoreKey + "_" + MasterAppTokenStoreKey
}
3 changes: 3 additions & 0 deletions core/keychain/keychain.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/99designs/keyring"
ri "github.com/FleekHQ/space-daemon/core/keychain/keyring"
"github.com/FleekHQ/space-daemon/core/permissions"
"github.com/FleekHQ/space-daemon/core/store"
"github.com/FleekHQ/space-daemon/log"
"github.com/libp2p/go-libp2p-core/crypto"
Expand Down Expand Up @@ -48,6 +49,8 @@ type Keychain interface {
Sign([]byte) ([]byte, error)
ImportExistingKeyPair(priv crypto.PrivKey, mnemonic string) error
DeleteKeypair() error
StoreAppToken(tok *permissions.AppToken) error
GetAppToken(key string) (*permissions.AppToken, error)
}

type keychainOptions struct {
Expand Down
108 changes: 108 additions & 0 deletions core/keychain/test/keychain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/99designs/keyring"
"github.com/FleekHQ/space-daemon/core/keychain"
"github.com/FleekHQ/space-daemon/core/permissions"
"github.com/FleekHQ/space-daemon/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -201,3 +202,110 @@ func TestKeychain_GetStoredMnemonic(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, mnemonic, mnemonic2)
}

func TestKeychain_AppToken_StoreMaster(t *testing.T) {
kc := initTestKeychain(t)

mockStore.On("Get", []byte(keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey)).Return(nil, nil)
mockKeyRing.On("Get", keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey).Return(keyring.Item{}, keyring.ErrKeyNotFound)
mockKeyRing.On("Set", mock.Anything).Return(nil)
mockStore.On("Set", mock.Anything, mock.Anything).Return(nil)

tok, err := permissions.GenerateRandomToken(true, []string{})
assert.NoError(t, err)

err = kc.StoreAppToken(tok)
assert.NoError(t, err)

marshalled, err := permissions.MarshalToken(tok)
assert.NoError(t, err)

mockKeyRing.AssertCalled(t, "Set", keyring.Item{
Key: keychain.AppTokenStoreKey + "_" + tok.Key,
Data: marshalled,
Label: "Space App - App Token",
})

mockKeyRing.AssertCalled(t, "Set", keyring.Item{
Key: keychain.AppTokenStoreKey + "_" + keychain.MasterAppTokenStoreKey,
Data: marshalled,
Label: "Space App - Master App Token",
})

mockStore.AssertCalled(t, "Set", []byte(keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey), []byte(tok.Key))
}

func TestKeychain_AppToken_StoreNonMaster(t *testing.T) {
kc := initTestKeychain(t)

mockStore.On("Get", []byte(keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey)).Return(nil, nil)
mockKeyRing.On("Get", keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey).Return(keyring.Item{}, keyring.ErrKeyNotFound)
mockKeyRing.On("Set", mock.Anything).Once().Return(nil)

tok, err := permissions.GenerateRandomToken(false, []string{})
assert.NoError(t, err)

err = kc.StoreAppToken(tok)
assert.NoError(t, err)

marshalled, err := permissions.MarshalToken(tok)
assert.NoError(t, err)

mockKeyRing.AssertCalled(t, "Set", keyring.Item{
Key: keychain.AppTokenStoreKey + "_" + tok.Key,
Data: marshalled,
Label: "Space App - App Token",
})

mockKeyRing.AssertNotCalled(t, "Set", keyring.Item{
Key: keychain.AppTokenStoreKey + "_" + keychain.MasterAppTokenStoreKey,
Data: marshalled,
Label: "Space App - Master App Token",
})
}

func TestKeychain_AppToken_StoreMasterOverride1(t *testing.T) {
kc := initTestKeychain(t)

tok, err := permissions.GenerateRandomToken(true, []string{})
assert.NoError(t, err)

mockStore.On("Get", []byte(keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey)).Return([]byte(tok.Key), nil)

err = kc.StoreAppToken(tok)
assert.Error(t, err)
}

func TestKeychain_AppToken_StoreMasterOverride2(t *testing.T) {
kc := initTestKeychain(t)

tok, err := permissions.GenerateRandomToken(true, []string{})
assert.NoError(t, err)

mockStore.On("Get", []byte(keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey)).Return(nil, nil)
mockKeyRing.On("Get", keychain.AppTokenStoreKey+"_"+keychain.MasterAppTokenStoreKey).Return(keyring.Item{}, nil)

err = kc.StoreAppToken(tok)
assert.Error(t, err)
}

func TestKeychain_AppToken_Get(t *testing.T) {
kc := initTestKeychain(t)

tok, err := permissions.GenerateRandomToken(false, []string{})
assert.NoError(t, err)

marshalled, err := permissions.MarshalToken(tok)
assert.NoError(t, err)

mockKeyRing.On("Get", keychain.AppTokenStoreKey+"_"+tok.Key).Return(keyring.Item{
Key: keychain.AppTokenStoreKey + "_" + tok.Key,
Data: marshalled,
Label: "Space App - App Token",
}, nil)

tok2, err := kc.GetAppToken(tok.Key)
assert.NoError(t, err)

assert.Equal(t, tok, tok2)
}
78 changes: 78 additions & 0 deletions core/permissions/app_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package permissions

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"strings"
)

var invalidAppTokenErr = errors.New("app token is invalid")

const tokenKeyLength = 20
const tokenSecretLength = 30

type AppToken struct {
Key string `json:"key"`
Secret string `json:"secret"`
IsMaster bool `json:"isMaster"`
Permissions []string `json:"permissions"`
}

func UnmarshalToken(marshalledToken []byte) (*AppToken, error) {
var result AppToken
err := json.Unmarshal(marshalledToken, &result)
if err != nil {
return nil, err
}

return &result, nil
}

func MarshalToken(tok *AppToken) ([]byte, error) {
jsonData, err := json.Marshal(tok)
if err != nil {
return nil, err
}

return jsonData, nil

}

func GenerateRandomToken(isMaster bool, permissions []string) (*AppToken, error) {
k := make([]byte, tokenKeyLength)
_, err := rand.Read(k)
if err != nil {
return nil, err
}

s := make([]byte, tokenSecretLength)
_, err = rand.Read(s)
if err != nil {
return nil, err
}

return &AppToken{
Key: base64.RawURLEncoding.EncodeToString(k),
Secret: base64.RawURLEncoding.EncodeToString(s),
IsMaster: isMaster,
Permissions: permissions,
}, nil
}

func (a *AppToken) GetAccessToken() string {
return a.Key + "." + a.Secret
}

func GetKeyAndSecretFromAccessToken(accessToken string) (key string, secret string, err error) {
tp := strings.Split(accessToken, ".")
if len(tp) < 2 {
return "", "", errors.New("invalid token format")
}

key = tp[0]
secret = tp[1]

return
}
32 changes: 32 additions & 0 deletions core/permissions/app_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package permissions_test

import (
"testing"

"github.com/FleekHQ/space-daemon/core/permissions"
"github.com/stretchr/testify/assert"
)

func TestPermissions_AppToken_Generation(t *testing.T) {
tok, err := permissions.GenerateRandomToken(true, []string{})
assert.NoError(t, err)

marshalled, err := permissions.MarshalToken(tok)
assert.NoError(t, err)
unmarshalled, err := permissions.UnmarshalToken(marshalled)
assert.NoError(t, err)

assert.Equal(t, tok, unmarshalled)
}

func TestPermissions_AppToken_GenerationWithPerms(t *testing.T) {
tok, err := permissions.GenerateRandomToken(false, []string{"OpenFile", "ListDirectories"})
assert.NoError(t, err)

marshalled, err := permissions.MarshalToken(tok)
assert.NoError(t, err)
unmarshalled, err := permissions.UnmarshalToken(marshalled)
assert.NoError(t, err)

assert.Equal(t, tok, unmarshalled)
}
16 changes: 16 additions & 0 deletions core/space/services/services_app_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package services

import (
"context"

"github.com/FleekHQ/space-daemon/core/permissions"
)

func (s *Space) InitializeMasterAppToken(ctx context.Context) (*permissions.AppToken, error) {
newAppToken, err := permissions.GenerateRandomToken(true, []string{})
if err != nil {
return nil, err
}

return newAppToken, s.keychain.StoreAppToken(newAppToken)
}
2 changes: 2 additions & 0 deletions core/space/space.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"io"

"github.com/FleekHQ/space-daemon/core/permissions"
"github.com/FleekHQ/space-daemon/core/textile/hub"
"github.com/FleekHQ/space-daemon/core/vault"
crypto "github.com/libp2p/go-libp2p-crypto"
Expand Down Expand Up @@ -62,6 +63,7 @@ type Service interface {
GetNotificationsLastSeenAt() (int64, error)
TruncateData(ctx context.Context) error
SearchFiles(ctx context.Context, query string) ([]domain.SearchFileEntry, error)
InitializeMasterAppToken(ctx context.Context) (*permissions.AppToken, error)
}

type serviceOptions struct {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/fatih/color v1.9.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.4.2
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1
github.com/google/uuid v1.1.1 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.14.6
github.com/ikawaha/kagome.ipadic v1.1.2 // indirect
Expand Down
Loading

0 comments on commit ac7c728

Please sign in to comment.