keyproxy.go 4.21 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
6
package dovecot

import (
	"context"
	"encoding/base64"
	"errors"
ale's avatar
ale committed
7
	"log"
ale's avatar
ale committed
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	"strings"

	"git.autistici.org/ai3/go-common/clientutil"

	"git.autistici.org/id/keystore/backend"
	"git.autistici.org/id/keystore/client"
	"git.autistici.org/id/keystore/userenckey"
)

// Config for the dovecot-keystore daemon.
type Config struct {
	Shard      string                    `yaml:"shard"`
	LDAPConfig *backend.LDAPConfig       `yaml:"ldap"`
	Keystore   *clientutil.BackendConfig `yaml:"keystore"`
22
23
24
25

	// Set this to true if the keys obtained from the backend need
	// to be base64-encoded before being sent to Dovecot.
	Base64Encode bool `yaml:"base64_encode_results"`
ale's avatar
ale committed
26
27
28
29
30
}

// Database represents the interface to the underlying backend for
// encrypted user keys.
type Database interface {
ale's avatar
ale committed
31
32
	GetPublicKey(context.Context, string) ([]byte, error)
	GetPrivateKeys(context.Context, string) ([][]byte, error)
ale's avatar
ale committed
33
34
35
}

func (c *Config) check() error {
36
37
38
	if c.Keystore == nil {
		return errors.New("missing keystore config")
	}
ale's avatar
ale committed
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
	if c.LDAPConfig == nil {
		return errors.New("missing backend config")
	}
	return c.LDAPConfig.Valid()
}

type userdbResponse struct {
	PublicKey string `json:"mail_crypt_global_public_key"`
}

type passdbResponse struct {
	PrivateKey string `json:"mail_crypt_global_private_key"`
}

var passwordSep = "/"

// KeyLookupProxy interfaces Dovecot with the user encryption key database.
type KeyLookupProxy struct {
	config   *Config
	keystore client.Client
	db       Database
}

62
// NewKeyLookupProxy returns a KeyLookupProxy with the specified configuration.
ale's avatar
ale committed
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
func NewKeyLookupProxy(config *Config) (*KeyLookupProxy, error) {
	if err := config.check(); err != nil {
		return nil, err
	}

	ksc, err := client.New(config.Keystore)
	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
	}

	return &KeyLookupProxy{
		config:   config,
		keystore: ksc,
		db:       ldap,
	}, nil
}

ale's avatar
ale committed
86
87
88
89
90
const (
	namespace    = "shared"
	namespaceLen = len(namespace)
)

ale's avatar
ale committed
91
92
93
// Lookup a key using the dovecot dict proxy interface.
//
// We can be sent a userdb lookup, or a passdb lookup, and we can tell
94
// them apart with the key prefix (passdb/ or userdb/).
ale's avatar
ale committed
95
func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool, error) {
96
	switch {
ale's avatar
ale committed
97
98
	case strings.HasPrefix(key, namespace+"/passdb/"):
		kparts := strings.SplitN(key[namespaceLen+8:], passwordSep, 2)
ale's avatar
ale committed
99
		return s.lookupPassdb(ctx, kparts[0], kparts[1])
ale's avatar
ale committed
100
101
	case strings.HasPrefix(key, namespace+"/userdb/"):
		return s.lookupUserdb(ctx, key[namespaceLen+8:])
102
	default:
ale's avatar
ale committed
103
		log.Printf("unknown key %s", key)
ale's avatar
Typo    
ale committed
104
		return nil, false, nil
ale's avatar
ale committed
105
106
107
	}
}

ale's avatar
ale committed
108
109
110
111
112
func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (interface{}, bool, error) {
	pub, err := s.db.GetPublicKey(ctx, username)
	if err != nil {
		return nil, false, err
	}
ale's avatar
ale committed
113
	if pub == nil {
ale's avatar
ale committed
114
115
		log.Printf("userdb lookup for %s (no keys)", username)
		return nil, false, nil
ale's avatar
ale committed
116
	}
ale's avatar
ale committed
117
	log.Printf("userdb lookup for %s", username)
118
	return &userdbResponse{PublicKey: s.b64encode(pub)}, true, nil
ale's avatar
ale committed
119
120
}

ale's avatar
ale committed
121
func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool, error) {
ale's avatar
ale committed
122
123
124
125
	// If the password is a SSO token, try to fetch the
	// unencrypted key from the keystore daemon.
	priv, err := s.keystore.Get(ctx, s.config.Shard, username, password)
	if err == nil {
ale's avatar
ale committed
126
		log.Printf("passdb lookup for %s (from keystore)", username)
127
		return &passdbResponse{PrivateKey: s.b64encode(priv)}, true, nil
ale's avatar
ale committed
128
129
130
131
	}

	// Otherwise, fetch encrypted keys from the db and attempt to
	// decrypt them.
ale's avatar
ale committed
132
133
134
135
	encKeys, err := s.db.GetPrivateKeys(ctx, username)
	if err != nil {
		return nil, false, err
	}
ale's avatar
ale committed
136
	if len(encKeys) == 0 {
ale's avatar
ale committed
137
		log.Printf("failed passdb lookup for %s (no keys)", username)
ale's avatar
ale committed
138
		return nil, false, nil
ale's avatar
ale committed
139
140
141
	}
	priv, err = userenckey.Decrypt(encKeys, []byte(password))
	if err != nil {
ale's avatar
ale committed
142
		log.Printf("failed passdb lookup for %s (could not decrypt key)", username)
ale's avatar
ale committed
143
		return nil, false, err
ale's avatar
ale committed
144
	}
ale's avatar
ale committed
145
	log.Printf("passdb lookup for %s (decrypted)", username)
146
	return &passdbResponse{PrivateKey: s.b64encode(priv)}, true, nil
ale's avatar
ale committed
147
148
}

149
150
151
152
153
func (s *KeyLookupProxy) b64encode(b []byte) string {
	if s.config.Base64Encode {
		return base64.StdEncoding.EncodeToString(b)
	}
	return string(b)
ale's avatar
ale committed
154
}