keyproxy.go 5.55 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
	"strings"

	"git.autistici.org/ai3/go-common/clientutil"
11
	"git.autistici.org/ai3/go-common/userenckey"
ale's avatar
ale committed
12 13

	"git.autistici.org/id/keystore/backend"
ale's avatar
ale committed
14 15
	ldapBE "git.autistici.org/id/keystore/backend/ldap"
	sqlBE "git.autistici.org/id/keystore/backend/sql"
ale's avatar
ale committed
16 17 18 19 20
	"git.autistici.org/id/keystore/client"
)

// Config for the dovecot-keystore daemon.
type Config struct {
ale's avatar
ale committed
21 22 23
	Shard    string                    `yaml:"shard"`
	Backend  *backend.Config           `yaml:"backend"`
	Keystore *clientutil.BackendConfig `yaml:"keystore"`
24 25 26 27

	// 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
28 29 30
}

func (c *Config) check() error {
31 32 33
	if c.Keystore == nil {
		return errors.New("missing keystore config")
	}
ale's avatar
ale committed
34
	if c.Backend == nil {
ale's avatar
ale committed
35 36
		return errors.New("missing backend config")
	}
ale's avatar
ale committed
37
	return nil
ale's avatar
ale committed
38 39
}

40 41 42 43 44 45 46
// The response returned to userdb lookups. It contains the user's
// public key as a global key for the mail_crypt plugin, and it sets
// mail_crypt_save_version to 2. The idea is that you would then set
// mail_crypt_save_version = 0 in the global Dovecot configuration,
// which would then disable encryption for users without encryption
// keys. For details on what this means, see
// https://wiki2.dovecot.org/Plugins/MailCrypt.
ale's avatar
ale committed
47
type userdbResponse struct {
48 49
	PublicKey   string `json:"mail_crypt_global_public_key"`
	SaveVersion int    `json:"mail_crypt_save_version"`
ale's avatar
ale committed
50 51
}

52 53 54 55 56 57 58 59 60 61 62 63
func newUserDBResponse(publicKey string) *userdbResponse {
	return &userdbResponse{
		PublicKey:   publicKey,
		SaveVersion: 2,
	}
}

// The response returned to passdb lookups. We return the user's
// private key and the mail_crypt_save_version attribute as userdb
// parameters (hence the 'userdb_' prefix), and set the noauthenticate
// bit to inform Dovecot that this lookup is only meant to provide
// additional data, not authentication.
ale's avatar
ale committed
64
type passdbResponse struct {
65 66 67
	PrivateKey  string `json:"userdb_mail_crypt_global_private_key"`
	SaveVersion int    `json:"userdb_mail_crypt_save_version"`
	NoAuth      bool   `json:"noauthenticate"`
ale's avatar
ale committed
68 69 70 71
}

func newPassDBResponse(privateKey string) *passdbResponse {
	return &passdbResponse{
72 73 74
		PrivateKey:  privateKey,
		SaveVersion: 2,
		NoAuth:      true,
ale's avatar
ale committed
75
	}
ale's avatar
ale committed
76 77 78 79 80 81 82 83
}

var passwordSep = "/"

// KeyLookupProxy interfaces Dovecot with the user encryption key database.
type KeyLookupProxy struct {
	config   *Config
	keystore client.Client
ale's avatar
ale committed
84
	db       backend.Database
ale's avatar
ale committed
85 86
}

87
// NewKeyLookupProxy returns a KeyLookupProxy with the specified configuration.
ale's avatar
ale committed
88 89 90 91 92 93 94 95 96 97
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
	}

ale's avatar
ale committed
98 99 100 101 102 103 104 105 106
	var db backend.Database
	switch config.Backend.Type {
	case "ldap":
		db, err = ldapBE.New(config.Backend.Params)
	case "sql":
		db, err = sqlBE.New(config.Backend.Params)
	default:
		err = errors.New("unknown backend type")
	}
ale's avatar
ale committed
107 108 109 110 111 112 113
	if err != nil {
		return nil, err
	}

	return &KeyLookupProxy{
		config:   config,
		keystore: ksc,
ale's avatar
ale committed
114
		db:       db,
ale's avatar
ale committed
115 116 117
	}, nil
}

118 119 120 121 122
const (
	namespace    = "shared"
	namespaceLen = len(namespace)
)

ale's avatar
ale committed
123 124 125
// Lookup a key using the dovecot dict proxy interface.
//
// We can be sent a userdb lookup, or a passdb lookup, and we can tell
126
// them apart with the key prefix (passdb/ or userdb/).
ale's avatar
ale committed
127
func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool, error) {
128
	switch {
129 130
	case strings.HasPrefix(key, namespace+"/passdb/"):
		kparts := strings.SplitN(key[namespaceLen+8:], passwordSep, 2)
ale's avatar
ale committed
131
		return s.lookupPassdb(ctx, kparts[0], kparts[1])
132 133
	case strings.HasPrefix(key, namespace+"/userdb/"):
		return s.lookupUserdb(ctx, key[namespaceLen+8:])
134
	default:
ale's avatar
ale committed
135
		log.Printf("unknown key %s", key)
ale's avatar
ale committed
136
		return nil, false, nil
ale's avatar
ale committed
137 138 139
	}
}

ale's avatar
ale committed
140 141 142 143 144
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
145
	if pub == nil {
ale's avatar
ale committed
146 147
		log.Printf("userdb lookup for %s (no keys)", username)
		return nil, false, nil
ale's avatar
ale committed
148
	}
ale's avatar
ale committed
149
	log.Printf("userdb lookup for %s", username)
150
	return newUserDBResponse(s.b64encode(pub)), true, nil
ale's avatar
ale committed
151 152
}

ale's avatar
ale committed
153
func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool, error) {
ale's avatar
ale committed
154 155 156
	// 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)
157 158 159
	if err != nil {
		log.Printf("keystore lookup for %s failed: %v", username, err)
	} else {
ale's avatar
ale committed
160
		log.Printf("passdb lookup for %s (from keystore)", username)
ale's avatar
ale committed
161
		return newPassDBResponse(s.b64encode(priv)), true, nil
ale's avatar
ale committed
162 163 164 165
	}

	// Otherwise, fetch encrypted keys from the db and attempt to
	// decrypt them.
ale's avatar
ale committed
166 167 168 169
	encKeys, err := s.db.GetPrivateKeys(ctx, username)
	if err != nil {
		return nil, false, err
	}
ale's avatar
ale committed
170
	if len(encKeys) == 0 {
ale's avatar
ale committed
171
		log.Printf("failed passdb lookup for %s (no keys)", username)
ale's avatar
ale committed
172
		return nil, false, nil
ale's avatar
ale committed
173
	}
174
	key, err := userenckey.Decrypt(encKeys, []byte(password))
ale's avatar
ale committed
175
	if err != nil {
ale's avatar
ale committed
176
		log.Printf("failed passdb lookup for %s (could not decrypt key)", username)
ale's avatar
ale committed
177
		return nil, false, err
ale's avatar
ale committed
178
	}
179 180 181 182 183
	priv, err = key.PEM()
	if err != nil {
		log.Printf("failed passdb lookup for %s (obtained invalid key: %v)", username, err)
		return nil, false, err
	}
ale's avatar
ale committed
184
	log.Printf("passdb lookup for %s (decrypted)", username)
ale's avatar
ale committed
185
	return newPassDBResponse(s.b64encode(priv)), true, nil
ale's avatar
ale committed
186 187
}

188 189 190 191 192
func (s *KeyLookupProxy) b64encode(b []byte) string {
	if s.config.Base64Encode {
		return base64.StdEncoding.EncodeToString(b)
	}
	return string(b)
ale's avatar
ale committed
193
}