keystore.go 4.87 KB
Newer Older
1
package server
ale's avatar
ale committed
2
3
4
5
6

import (
	"context"
	"errors"
	"io/ioutil"
7
	"log"
ale's avatar
ale committed
8
9
10
11
12
13
14
	"strings"
	"sync"
	"time"

	"git.autistici.org/id/go-sso"

	"git.autistici.org/id/keystore/backend"
ale's avatar
ale committed
15
	"git.autistici.org/id/keystore/userenckey"
ale's avatar
ale committed
16
17
18
)

var (
19
20
21
22
	errNoKeys       = errors.New("no keys available")
	errBadUser      = errors.New("username does not match authentication token")
	errUnauthorized = errors.New("unauthorized")
	errInvalidTTL   = errors.New("invalid ttl")
ale's avatar
ale committed
23
24
25
26
27
)

// Database represents the interface to the underlying backend for
// encrypted user keys.
type Database interface {
ale's avatar
ale committed
28
	GetPrivateKeys(context.Context, string) ([][]byte, error)
ale's avatar
ale committed
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
}

type userKey struct {
	pkey   []byte
	expiry time.Time
}

// Config for the KeyStore.
type Config struct {
	SSOPublicKeyFile string `yaml:"sso_public_key_file"`
	SSOService       string `yaml:"sso_service"`
	SSODomain        string `yaml:"sso_domain"`

	//Backend string `yaml:"backend"`
	LDAPConfig *backend.LDAPConfig `yaml:"ldap"`
}

func (c *Config) check() error {
	if c.SSOService == "" {
		return errors.New("sso_service is empty")
	}
	if !strings.HasSuffix(c.SSOService, "/") {
		return errors.New("sso_service is invalid (does not end with /)")
	}
	if c.SSODomain == "" {
		return errors.New("sso_domain is empty")
	}
	if c.LDAPConfig == nil {
		return errors.New("missing backend config")
	}
ale's avatar
ale committed
59
	return c.LDAPConfig.Valid()
ale's avatar
ale committed
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
}

// KeyStore holds decrypted secrets for users in memory for a short
// time (of the order of a SSO session lifespan). User secrets can be
// opened with a password (used to decrypt the key, which is stored
// encrypted in a database), queried, and closed (forgotten).
//
// The database can provide multiple versions of the encrypted key (to
// support multiple decryption passwords), in which case we'll try
// them all sequentially until one of them decrypts successfully with
// the provided password.
//
// In order to query the KeyStore, you need to present a valid SSO
// token for the user whose secrets you would like to obtain.
//
type KeyStore struct {
	mx       sync.Mutex
	userKeys map[string]userKey

	db        Database
	service   string
	validator sso.Validator
}

84
85
// NewKeyStore creates a new KeyStore with the given config and returns it.
func NewKeyStore(config *Config) (*KeyStore, error) {
ale's avatar
ale committed
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
	if err := config.check(); err != nil {
		return nil, err
	}

	ssoKey, err := ioutil.ReadFile(config.SSOPublicKeyFile)
	if err != nil {
		return nil, err
	}
	v, err := sso.NewValidator(ssoKey, config.SSODomain)
	if err != nil {
		return nil, err
	}

	// There is only one supported backend type, ldap.
	ldap, err := backend.NewLDAPBackend(config.LDAPConfig)
	if err != nil {
		return nil, err
	}

	s := &KeyStore{
		userKeys:  make(map[string]userKey),
		service:   config.SSOService,
		validator: v,
		db:        ldap,
	}
	go s.expire()
	return s, nil
}

func (s *KeyStore) expire() {
	for t := range time.NewTicker(600 * time.Second).C {
		s.mx.Lock()
		for u, k := range s.userKeys {
			if k.expiry.After(t) {
				wipeBytes(k.pkey)
				delete(s.userKeys, u)
			}
		}
		s.mx.Unlock()
	}
}

// Open the user's key store with the given password. If successful,
// the unencrypted user key will be stored for at most ttlSeconds, or
// until Close is called.
//
// A Context is needed because this method might issue an RPC.
func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSeconds int) error {
	if ttlSeconds == 0 {
135
		return errInvalidTTL
ale's avatar
ale committed
136
137
	}

ale's avatar
ale committed
138
139
140
141
	encKeys, err := s.db.GetPrivateKeys(ctx, username)
	if err != nil {
		return err
	}
ale's avatar
ale committed
142
	if len(encKeys) == 0 {
143
144
		// No keys found. Not an error.
		return nil
ale's avatar
ale committed
145
146
	}

ale's avatar
ale committed
147
148
149
	// Naive and inefficient way of decrypting multiple keys: it
	// will recompute the kdf every time, which is expensive.
	pkey, err := userenckey.Decrypt(encKeys, []byte(password))
ale's avatar
ale committed
150
	if err != nil {
ale's avatar
ale committed
151
		return err
ale's avatar
ale committed
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
	}

	s.mx.Lock()
	s.userKeys[username] = userKey{
		pkey:   pkey,
		expiry: time.Now().Add(time.Duration(ttlSeconds) * time.Second),
	}
	s.mx.Unlock()
	return nil
}

// Get the unencrypted key for the specified user. The caller needs to
// provide a valid SSO ticket for the user.
func (s *KeyStore) Get(username, ssoTicket string) ([]byte, error) {
	// Validate the SSO ticket.
	tkt, err := s.validator.Validate(ssoTicket, "", s.service, nil)
	if err != nil {
169
170
171
		// Log authentication failures for debugging purposes.
		log.Printf("Validate(%s) error: %v", username, err)
		return nil, errUnauthorized
ale's avatar
ale committed
172
173
	}
	if tkt.User != username {
174
175
		log.Printf("Validate(%s) user mismatch: sso=%s", username, tkt.User)
		return nil, errBadUser
ale's avatar
ale committed
176
177
178
179
180
181
	}

	s.mx.Lock()
	defer s.mx.Unlock()
	u, ok := s.userKeys[username]
	if !ok {
182
		return nil, errNoKeys
ale's avatar
ale committed
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
	}
	return u.pkey, nil
}

// Close the user's key store and wipe the associated unencrypted key
// from memory.
func (s *KeyStore) Close(username string) {
	s.mx.Lock()
	if k, ok := s.userKeys[username]; ok {
		wipeBytes(k.pkey)
		delete(s.userKeys, username)
	}
	s.mx.Unlock()
}

func wipeBytes(b []byte) {
	for i := 0; i < len(b); i++ {
		b[i] = 0
	}
}